remdb 0.3.242__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of remdb might be problematic. Click here for more details.

Files changed (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,820 @@
1
+ """
2
+ MCP Resources for REM system information.
3
+
4
+ Resources are read-only data sources that LLMs can access for context.
5
+ They provide schema information, documentation, and system status.
6
+
7
+ Design Pattern:
8
+ - Resources are registered with the FastMCP server
9
+ - Resources return structured data (typically as strings or JSON)
10
+ - Resources don't modify system state (read-only)
11
+ - Resources help LLMs understand available operations
12
+
13
+ Available Resources:
14
+ - rem://schema/entities - Entity schemas documentation
15
+ - rem://schema/query-types - REM query types documentation
16
+ - rem://status - System health and statistics
17
+ """
18
+
19
+ from fastmcp import FastMCP
20
+
21
+
22
+ def register_schema_resources(mcp: FastMCP):
23
+ """
24
+ Register schema documentation resources.
25
+
26
+ Args:
27
+ mcp: FastMCP server instance
28
+ """
29
+
30
+ @mcp.resource("rem://schema/entities")
31
+ def get_entity_schemas() -> str:
32
+ """
33
+ Get REM entity schemas documentation.
34
+
35
+ Returns complete schema information for all entity types:
36
+ - Resource: Chunked, embedded content
37
+ - Entity: Domain knowledge nodes
38
+ - Moment: Temporal narratives
39
+ - Message: Conversation messages
40
+ - User: System users
41
+ - File: File uploads
42
+ """
43
+ return """
44
+ # REM Entity Schemas
45
+
46
+ ## Resource
47
+ Chunked, embedded content from documents, files, conversations.
48
+
49
+ Fields:
50
+ - id: UUID (auto-generated)
51
+ - user_id: User identifier (primary data scoping field)
52
+ - tenant_id: Tenant identifier (legacy, set to user_id)
53
+ - name: Resource name/label (used for LOOKUP)
54
+ - content: Main text content
55
+ - category: Optional category (document, conversation, etc.)
56
+ - related_entities: JSONB array of extracted entity references
57
+ - graph_paths: JSONB array of InlineEdge objects
58
+ - resource_timestamp: Timestamp of resource creation
59
+ - metadata: JSONB flexible metadata dict
60
+ - created_at, updated_at, deleted_at: Temporal tracking
61
+
62
+ ## Entity
63
+ Domain knowledge nodes with properties and relationships.
64
+
65
+ NOTE: Entities are stored within resources/moments, not in a separate table.
66
+ Entity IDs are human-readable labels (e.g., "sarah-chen", "api-design-v2").
67
+
68
+ ## Moment
69
+ Temporal narratives and time-bound events.
70
+
71
+ Fields:
72
+ - id: UUID (auto-generated)
73
+ - user_id: User identifier (primary data scoping field)
74
+ - tenant_id: Tenant identifier (legacy, set to user_id)
75
+ - name: Moment name/label (used for LOOKUP)
76
+ - moment_type: Type (meeting, coding_session, conversation, etc.)
77
+ - resource_timestamp: Start time
78
+ - resource_ends_timestamp: End time
79
+ - present_persons: JSONB array of Person objects
80
+ - speakers: JSONB array of Speaker objects
81
+ - emotion_tags: Array of emotion tags
82
+ - topic_tags: Array of topic tags
83
+ - summary: Natural language summary
84
+ - source_resource_ids: Array of referenced resource UUIDs
85
+ - created_at, updated_at, deleted_at: Temporal tracking
86
+
87
+ ## Message
88
+ Conversation messages with agents.
89
+
90
+ Fields:
91
+ - id: UUID (auto-generated)
92
+ - user_id: User identifier (primary data scoping field)
93
+ - tenant_id: Tenant identifier (legacy, set to user_id)
94
+ - role: Message role (user, assistant, system)
95
+ - content: Message text
96
+ - session_id: Conversation session identifier
97
+ - metadata: JSONB flexible metadata dict
98
+ - created_at, updated_at, deleted_at: Temporal tracking
99
+
100
+ ## User
101
+ System users with authentication.
102
+
103
+ Fields:
104
+ - id: UUID (auto-generated)
105
+ - user_id: User identifier (primary data scoping field)
106
+ - tenant_id: Tenant identifier (legacy, set to user_id)
107
+ - name: User name
108
+ - email: User email
109
+ - metadata: JSONB flexible metadata dict
110
+ - created_at, updated_at, deleted_at: Temporal tracking
111
+
112
+ ## File
113
+ File uploads with S3 storage.
114
+
115
+ Fields:
116
+ - id: UUID (auto-generated)
117
+ - user_id: User identifier (primary data scoping field)
118
+ - tenant_id: Tenant identifier (legacy, set to user_id)
119
+ - name: File name
120
+ - s3_key: S3 object key
121
+ - s3_bucket: S3 bucket name
122
+ - content_type: MIME type
123
+ - size_bytes: File size
124
+ - metadata: JSONB flexible metadata dict
125
+ - created_at, updated_at, deleted_at: Temporal tracking
126
+ """
127
+
128
+ @mcp.resource("rem://schema/query-types")
129
+ def get_query_types() -> str:
130
+ """
131
+ Get REM query types documentation.
132
+
133
+ Returns comprehensive documentation for all REM query types
134
+ with examples and parameter specifications.
135
+ """
136
+ return """
137
+ # REM Query Types
138
+
139
+ ## LOOKUP
140
+ O(1) entity resolution across ALL tables using KV_STORE.
141
+
142
+ Parameters:
143
+ - entity_key (required): Entity label/name (e.g., "sarah-chen", "api-design-v2")
144
+ - user_id (optional): User scoping for private entities
145
+
146
+ Example:
147
+ ```
148
+ rem_query(query_type="lookup", entity_key="Sarah Chen", user_id="user-123")
149
+ ```
150
+
151
+ Returns:
152
+ - entity_key: The looked-up key
153
+ - entity_type: Entity type (person, document, etc.)
154
+ - entity_id: UUID of the entity
155
+ - content_summary: Summary of entity content
156
+ - metadata: Additional metadata
157
+
158
+ ## FUZZY
159
+ Fuzzy text matching using pg_trgm similarity.
160
+
161
+ Parameters:
162
+ - query_text (required): Query string
163
+ - threshold (optional): Similarity threshold 0.0-1.0 (default: 0.7)
164
+ - limit (optional): Max results (default: 10)
165
+ - user_id (optional): User scoping
166
+
167
+ Example:
168
+ ```
169
+ rem_query(query_type="fuzzy", query_text="sara", threshold=0.7, user_id="user-123")
170
+ ```
171
+
172
+ Returns:
173
+ - Entities matching query with similarity scores
174
+ - Ordered by similarity (highest first)
175
+
176
+ ## SEARCH
177
+ Semantic vector search using embeddings (table-specific).
178
+
179
+ Parameters:
180
+ - query_text (required): Natural language query
181
+ - table_name (required): Table to search (resources, moments, etc.)
182
+ - field_name (optional): Field to search (defaults to "content")
183
+ - provider (optional): Embedding provider (default: from LLM__EMBEDDING_PROVIDER setting)
184
+ - min_similarity (optional): Minimum similarity 0.0-1.0 (default: 0.3)
185
+ - limit (optional): Max results (default: 10)
186
+ - user_id (optional): User scoping
187
+
188
+ Example:
189
+ ```
190
+ rem_query(
191
+ query_type="search",
192
+ query_text="database migration",
193
+ table_name="resources",
194
+ user_id="user-123"
195
+ )
196
+ ```
197
+
198
+ Returns:
199
+ - Semantically similar entities
200
+ - Ordered by similarity score
201
+
202
+ ## SQL
203
+ Direct SQL queries with WHERE clauses (tenant-scoped).
204
+
205
+ Parameters:
206
+ - table_name (required): Table to query
207
+ - where_clause (optional): SQL WHERE condition
208
+ - limit (optional): Max results
209
+
210
+ Example:
211
+ ```
212
+ rem_query(
213
+ query_type="sql",
214
+ table_name="moments",
215
+ where_clause="moment_type='meeting' AND resource_timestamp > '2025-01-01'",
216
+ user_id="user-123"
217
+ )
218
+ ```
219
+
220
+ Returns:
221
+ - Matching rows from table
222
+ - Automatically scoped to user_id
223
+
224
+ ## TRAVERSE
225
+ Multi-hop graph traversal with depth control.
226
+
227
+ Parameters:
228
+ - start_key (required): Starting entity key
229
+ - max_depth (optional): Maximum traversal depth (default: 1)
230
+ - depth=0: PLAN mode (analyze edges without traversal)
231
+ - depth=1+: Full traversal with cycle detection
232
+ - rel_type (optional): Filter by relationship type (e.g., "manages", "authored_by")
233
+ - user_id (optional): User scoping
234
+
235
+ Example:
236
+ ```
237
+ rem_query(
238
+ query_type="traverse",
239
+ start_key="Sarah Chen",
240
+ max_depth=2,
241
+ rel_type="manages",
242
+ user_id="user-123"
243
+ )
244
+ ```
245
+
246
+ Returns:
247
+ - Traversed entities with depth info
248
+ - Relationship types and weights
249
+ - Path information for each node
250
+
251
+ ## Multi-Turn Exploration
252
+
253
+ REM supports iterated retrieval where LLMs conduct multi-turn conversations
254
+ with the database:
255
+
256
+ Turn 1: Find entry point
257
+ ```
258
+ LOOKUP "Sarah Chen"
259
+ ```
260
+
261
+ Turn 2: Analyze neighborhood (PLAN mode)
262
+ ```
263
+ TRAVERSE start_key="Sarah Chen" max_depth=0
264
+ ```
265
+
266
+ Turn 3: Selective traversal
267
+ ```
268
+ TRAVERSE start_key="Sarah Chen" rel_type="manages" max_depth=2
269
+ ```
270
+ """
271
+
272
+
273
+ def register_agent_resources(mcp: FastMCP):
274
+ """
275
+ Register agent schema resources.
276
+
277
+ Args:
278
+ mcp: FastMCP server instance
279
+ """
280
+
281
+ @mcp.resource("rem://agents")
282
+ def list_available_agents() -> str:
283
+ """
284
+ List all available agent schemas.
285
+
286
+ Returns a list of agent schemas packaged with REM, including:
287
+ - Agent name
288
+ - Description
289
+ - Available tools
290
+ - Version information
291
+
292
+ TODO: Add pagination support if agent count grows large (not needed for now)
293
+ """
294
+ import importlib.resources
295
+ import yaml
296
+ from pathlib import Path
297
+
298
+ try:
299
+ # Find packaged agent schemas
300
+ agents_ref = importlib.resources.files("rem") / "schemas" / "agents"
301
+ agents_dir = Path(str(agents_ref))
302
+
303
+ if not agents_dir.exists():
304
+ return "# Available Agents\n\nNo agent schemas found in package."
305
+
306
+ # Discover all agent schemas recursively
307
+ agent_files = sorted(agents_dir.rglob("*.yaml")) + sorted(agents_dir.rglob("*.yml")) + sorted(agents_dir.rglob("*.json"))
308
+
309
+ if not agent_files:
310
+ return "# Available Agents\n\nNo agent schemas found."
311
+
312
+ output = ["# Available Agent Schemas\n"]
313
+ output.append("Packaged agent schemas available for use:\n")
314
+
315
+ for agent_file in agent_files:
316
+ try:
317
+ with open(agent_file, "r") as f:
318
+ schema = yaml.safe_load(f)
319
+
320
+ agent_name = agent_file.stem
321
+ description = schema.get("description", "No description")
322
+ # Get first 200 characters of description
323
+ desc_snippet = description[:200] + "..." if len(description) > 200 else description
324
+
325
+ # Get additional metadata
326
+ extra = schema.get("json_schema_extra", {})
327
+ version = extra.get("version", "unknown")
328
+ tools = extra.get("tools", [])
329
+
330
+ output.append(f"\n## {agent_name}")
331
+ output.append(f"**Path:** `agents/{agent_file.name}`")
332
+ output.append(f"**Version:** {version}")
333
+ output.append(f"**Description:** {desc_snippet}")
334
+ if tools:
335
+ output.append(f"**Tools:** {', '.join(tools[:5])}" + (" ..." if len(tools) > 5 else ""))
336
+
337
+ # Usage example
338
+ output.append(f"\n**Usage:**")
339
+ output.append(f"```python")
340
+ output.append(f'rem ask agents/{agent_file.name} "Your query here"')
341
+ output.append(f"```")
342
+
343
+ except Exception as e:
344
+ output.append(f"\n## {agent_file.stem}")
345
+ output.append(f"⚠️ Error loading schema: {e}")
346
+
347
+ return "\n".join(output)
348
+
349
+ except Exception as e:
350
+ return f"# Available Agents\n\nError listing agents: {e}"
351
+
352
+ @mcp.resource("rem://agents/{agent_name}")
353
+ def get_agent_schema(agent_name: str) -> str:
354
+ """
355
+ Get a specific agent schema by name.
356
+
357
+ Args:
358
+ agent_name: Name of the agent (e.g., "ask_rem", "agent-builder")
359
+
360
+ Returns:
361
+ Full agent schema as YAML string, or error message if not found.
362
+ """
363
+ import importlib.resources
364
+ import yaml
365
+ from pathlib import Path
366
+
367
+ try:
368
+ # Find packaged agent schemas
369
+ agents_ref = importlib.resources.files("rem") / "schemas" / "agents"
370
+ agents_dir = Path(str(agents_ref))
371
+
372
+ if not agents_dir.exists():
373
+ return f"# Agent Not Found\n\nNo agent schemas directory found."
374
+
375
+ # Search for agent file (try multiple extensions)
376
+ for ext in [".yaml", ".yml", ".json"]:
377
+ # Try exact match first
378
+ agent_file = agents_dir / f"{agent_name}{ext}"
379
+ if agent_file.exists():
380
+ with open(agent_file, "r") as f:
381
+ content = f.read()
382
+ return f"# Agent Schema: {agent_name}\n\n```yaml\n{content}\n```"
383
+
384
+ # Try recursive search
385
+ matches = list(agents_dir.rglob(f"{agent_name}{ext}"))
386
+ if matches:
387
+ with open(matches[0], "r") as f:
388
+ content = f.read()
389
+ return f"# Agent Schema: {agent_name}\n\n```yaml\n{content}\n```"
390
+
391
+ # Not found - list available agents
392
+ available = [f.stem for f in agents_dir.rglob("*.yaml")] + \
393
+ [f.stem for f in agents_dir.rglob("*.yml")]
394
+ return f"# Agent Not Found\n\nAgent '{agent_name}' not found.\n\nAvailable agents: {', '.join(sorted(set(available)))}"
395
+
396
+ except Exception as e:
397
+ return f"# Error\n\nError loading agent '{agent_name}': {e}"
398
+
399
+
400
+ def register_file_resources(mcp: FastMCP):
401
+ """
402
+ Register file operation resources.
403
+
404
+ Args:
405
+ mcp: FastMCP server instance
406
+ """
407
+
408
+ @mcp.resource("rem://files/presigned-url/{s3_key}")
409
+ def get_presigned_url(s3_key: str, expiration: int = 3600) -> str:
410
+ """
411
+ Generate presigned URL for S3 object download.
412
+
413
+ Args:
414
+ s3_key: S3 object key (e.g., "tenant/files/uuid/file.pdf")
415
+ expiration: URL expiration time in seconds (default: 3600 = 1 hour)
416
+
417
+ Returns:
418
+ Presigned URL for downloading the file
419
+
420
+ Raises:
421
+ RuntimeError: If S3 is not configured
422
+
423
+ Example:
424
+ >>> url = get_presigned_url("acme/files/123/document.pdf")
425
+ >>> # Returns: https://s3.amazonaws.com/bucket/acme/files/123/document.pdf?signature=...
426
+ """
427
+ from ...settings import settings
428
+
429
+ # Check if S3 is configured
430
+ if not settings.s3.bucket_name:
431
+ raise RuntimeError(
432
+ "S3 is not configured. Cannot generate presigned URLs.\n"
433
+ "Configure S3 settings in ~/.rem/config.yaml or environment variables."
434
+ )
435
+
436
+ import aioboto3
437
+ import asyncio
438
+ from botocore.exceptions import ClientError
439
+
440
+ async def _generate_url():
441
+ session = aioboto3.Session()
442
+ async with session.client(
443
+ "s3",
444
+ endpoint_url=settings.s3.endpoint_url,
445
+ aws_access_key_id=settings.s3.access_key_id,
446
+ aws_secret_access_key=settings.s3.secret_access_key,
447
+ region_name=settings.s3.region,
448
+ ) as s3_client:
449
+ try:
450
+ url = await s3_client.generate_presigned_url(
451
+ "get_object",
452
+ Params={
453
+ "Bucket": settings.s3.bucket_name,
454
+ "Key": s3_key,
455
+ },
456
+ ExpiresIn=expiration,
457
+ )
458
+ return url
459
+ except ClientError as e:
460
+ raise RuntimeError(f"Failed to generate presigned URL: {e}")
461
+
462
+ # Run async function
463
+ loop = asyncio.get_event_loop()
464
+ if loop.is_running():
465
+ # If already in async context, create new loop
466
+ import nest_asyncio
467
+ nest_asyncio.apply()
468
+ url = loop.run_until_complete(_generate_url())
469
+ else:
470
+ url = asyncio.run(_generate_url())
471
+
472
+ return url
473
+
474
+
475
+ def register_status_resources(mcp: FastMCP):
476
+ """
477
+ Register system status resources.
478
+
479
+ Args:
480
+ mcp: FastMCP server instance
481
+ """
482
+
483
+ @mcp.resource("rem://status")
484
+ def get_system_status() -> str:
485
+ """
486
+ Get REM system health and statistics.
487
+
488
+ Returns system information including:
489
+ - Service health
490
+ - Database connection status
491
+ - Environment configuration
492
+ - Available query types
493
+ """
494
+ from ...settings import settings
495
+
496
+ return f"""
497
+ # REM System Status
498
+
499
+ ## Environment
500
+ - Environment: {settings.environment}
501
+ - Team: {settings.team}
502
+ - Root Path: {settings.root_path or '/'}
503
+
504
+ ## LLM Configuration
505
+ - Default Model: {settings.llm.default_model}
506
+ - Default Temperature: {settings.llm.default_temperature}
507
+ - Embedding Provider: {settings.llm.embedding_provider}
508
+ - Embedding Model: {settings.llm.embedding_model}
509
+ - OpenAI API Key: {"✓ Configured" if settings.llm.openai_api_key else "✗ Not configured"}
510
+ - Anthropic API Key: {"✓ Configured" if settings.llm.anthropic_api_key else "✗ Not configured"}
511
+
512
+ ## Database
513
+ - PostgreSQL: {settings.postgres.connection_string}
514
+
515
+ ## S3 Storage
516
+ - Bucket: {settings.s3.bucket_name}
517
+ - Region: {settings.s3.region}
518
+
519
+ ## Observability
520
+ - OTEL Enabled: {settings.otel.enabled}
521
+ - Phoenix Enabled: {settings.phoenix.enabled}
522
+
523
+ ## Authentication
524
+ - Auth Enabled: {settings.auth.enabled}
525
+
526
+ ## Available Query Types
527
+ - LOOKUP: O(1) entity resolution
528
+ - FUZZY: Fuzzy text matching
529
+ - SEARCH: Semantic vector search
530
+ - SQL: Direct SQL queries
531
+ - TRAVERSE: Multi-hop graph traversal
532
+
533
+ ## MCP Tools
534
+ - search_rem: Execute REM queries (LOOKUP, FUZZY, SEARCH, SQL, TRAVERSE)
535
+ - ask_rem_agent: Natural language to REM query conversion
536
+ - ingest_into_rem: File ingestion pipeline
537
+ - read_resource: Access MCP resources
538
+
539
+ ## Status
540
+ ✓ System operational
541
+ ✓ Ready to process queries
542
+ """
543
+
544
+
545
+ def register_session_resources(mcp: FastMCP):
546
+ """
547
+ Register session resources for loading conversation history.
548
+
549
+ Args:
550
+ mcp: FastMCP server instance
551
+ """
552
+
553
+ @mcp.resource("rem://sessions/{session_id}")
554
+ async def get_session_messages(session_id: str) -> str:
555
+ """
556
+ Load a conversation session by ID.
557
+
558
+ Returns the full message history including user messages, assistant responses,
559
+ and tool calls. Useful for evaluators and analysis agents.
560
+
561
+ Args:
562
+ session_id: Session UUID or identifier
563
+
564
+ Returns:
565
+ Formatted conversation history as markdown string with:
566
+ - Message type (user/assistant/tool)
567
+ - Content
568
+ - Timestamps
569
+ - Tool call details (if any)
570
+ """
571
+ from ...services.postgres import get_postgres_service
572
+
573
+ pg = get_postgres_service()
574
+ await pg.connect()
575
+
576
+ try:
577
+ # Query messages for session
578
+ query = """
579
+ SELECT id, message_type, content, metadata, created_at
580
+ FROM messages
581
+ WHERE session_id = $1
582
+ ORDER BY created_at ASC
583
+ """
584
+ messages = await pg.fetch(query, session_id)
585
+
586
+ if not messages:
587
+ return f"# Session Not Found\n\nNo messages found for session_id: {session_id}"
588
+
589
+ # Format output
590
+ output = [f"# Session: {session_id}\n"]
591
+ output.append(f"**Total messages:** {len(messages)}\n")
592
+
593
+ for i, msg in enumerate(messages, 1):
594
+ msg_type = msg['message_type']
595
+ content = msg['content'] or "(empty)"
596
+ created = msg['created_at']
597
+ metadata = msg.get('metadata') or {}
598
+
599
+ # Format based on message type
600
+ if msg_type == 'user':
601
+ output.append(f"\n## [{i}] USER ({created})")
602
+ output.append(f"```\n{content[:1000]}{'...' if len(content) > 1000 else ''}\n```")
603
+ elif msg_type == 'assistant':
604
+ output.append(f"\n## [{i}] ASSISTANT ({created})")
605
+ output.append(f"```\n{content[:1000]}{'...' if len(content) > 1000 else ''}\n```")
606
+ elif msg_type == 'tool':
607
+ tool_name = metadata.get('tool_name', 'unknown')
608
+ output.append(f"\n## [{i}] TOOL: {tool_name} ({created})")
609
+ # Truncate tool results more aggressively
610
+ output.append(f"```json\n{content[:500]}{'...' if len(content) > 500 else ''}\n```")
611
+ else:
612
+ output.append(f"\n## [{i}] {msg_type.upper()} ({created})")
613
+ output.append(f"```\n{content[:500]}{'...' if len(content) > 500 else ''}\n```")
614
+
615
+ return "\n".join(output)
616
+
617
+ finally:
618
+ await pg.disconnect()
619
+
620
+ @mcp.resource("rem://sessions")
621
+ async def list_recent_sessions() -> str:
622
+ """
623
+ List recent sessions with basic info.
624
+
625
+ Returns the most recent 20 sessions with:
626
+ - Session ID
627
+ - First user message (preview)
628
+ - Message count
629
+ - Timestamp
630
+ """
631
+ from ...services.postgres import get_postgres_service
632
+
633
+ pg = get_postgres_service()
634
+ await pg.connect()
635
+
636
+ try:
637
+ # Query recent sessions
638
+ query = """
639
+ SELECT
640
+ session_id,
641
+ MIN(created_at) as started_at,
642
+ COUNT(*) as message_count,
643
+ MIN(CASE WHEN message_type = 'user' THEN content END) as first_message
644
+ FROM messages
645
+ WHERE session_id IS NOT NULL
646
+ GROUP BY session_id
647
+ ORDER BY MIN(created_at) DESC
648
+ LIMIT 20
649
+ """
650
+ sessions = await pg.fetch(query)
651
+
652
+ if not sessions:
653
+ return "# Recent Sessions\n\nNo sessions found."
654
+
655
+ output = ["# Recent Sessions\n"]
656
+ output.append(f"Showing {len(sessions)} most recent sessions:\n")
657
+
658
+ for session in sessions:
659
+ session_id = session['session_id']
660
+ started = session['started_at']
661
+ count = session['message_count']
662
+ first_msg = session['first_message'] or "(no user message)"
663
+ preview = first_msg[:80] + "..." if len(first_msg) > 80 else first_msg
664
+
665
+ output.append(f"\n## {session_id}")
666
+ output.append(f"- **Started:** {started}")
667
+ output.append(f"- **Messages:** {count}")
668
+ output.append(f"- **First message:** {preview}")
669
+ output.append(f"- **Load:** `rem://sessions/{session_id}`")
670
+
671
+ return "\n".join(output)
672
+
673
+ finally:
674
+ await pg.disconnect()
675
+
676
+
677
+ def register_user_resources(mcp: FastMCP):
678
+ """
679
+ Register user profile resources for on-demand profile loading.
680
+
681
+ Args:
682
+ mcp: FastMCP server instance
683
+ """
684
+
685
+ @mcp.resource("user://profile/{user_id}")
686
+ async def get_user_profile(user_id: str) -> str:
687
+ """
688
+ Load a user's profile by ID.
689
+
690
+ Returns the user's profile information including:
691
+ - Email and name
692
+ - Summary (AI-generated profile summary)
693
+ - Interests and preferred topics
694
+ - Activity level
695
+
696
+ This resource is protected - each user can only access their own profile.
697
+ The user_id should match the authenticated user's ID from the JWT token.
698
+
699
+ Args:
700
+ user_id: User UUID from authentication
701
+
702
+ Returns:
703
+ Formatted user profile as markdown string, or error if not found
704
+ """
705
+ from ...services.postgres import get_postgres_service
706
+ from ...services.postgres.repository import Repository
707
+ from ...models.entities.user import User
708
+
709
+ pg = get_postgres_service()
710
+ await pg.connect()
711
+
712
+ try:
713
+ user_repo = Repository(User, "users", db=pg)
714
+ # Look up user by ID (user_id from JWT is the primary key)
715
+ user = await user_repo.get_by_id(user_id, tenant_id=None)
716
+
717
+ if not user:
718
+ return f"# User Profile Not Found\n\nNo user found with ID: {user_id}"
719
+
720
+ # Build profile output
721
+ output = [f"# User Profile: {user.name or user.email or 'Unknown'}"]
722
+ output.append("")
723
+
724
+ if user.email:
725
+ output.append(f"**Email:** {user.email}")
726
+
727
+ if user.role:
728
+ output.append(f"**Role:** {user.role}")
729
+
730
+ if user.tier:
731
+ output.append(f"**Tier:** {user.tier.value if hasattr(user.tier, 'value') else user.tier}")
732
+
733
+ if user.summary:
734
+ output.append(f"\n## Summary\n{user.summary}")
735
+
736
+ if user.interests:
737
+ output.append(f"\n## Interests\n- " + "\n- ".join(user.interests[:10]))
738
+
739
+ if user.preferred_topics:
740
+ output.append(f"\n## Preferred Topics\n- " + "\n- ".join(user.preferred_topics[:10]))
741
+
742
+ if user.activity_level:
743
+ output.append(f"\n**Activity Level:** {user.activity_level}")
744
+
745
+ if user.last_active_at:
746
+ output.append(f"**Last Active:** {user.last_active_at}")
747
+
748
+ # Add metadata if present (but redact sensitive fields)
749
+ if user.metadata:
750
+ safe_metadata = {k: v for k, v in user.metadata.items()
751
+ if k not in ('login_code', 'password', 'token', 'secret')}
752
+ if safe_metadata:
753
+ output.append(f"\n## Additional Info")
754
+ for key, value in list(safe_metadata.items())[:5]:
755
+ output.append(f"- **{key}:** {value}")
756
+
757
+ return "\n".join(output)
758
+
759
+ except Exception as e:
760
+ return f"# Error Loading Profile\n\nFailed to load user profile: {e}"
761
+
762
+ finally:
763
+ await pg.disconnect()
764
+
765
+
766
+ # Resource dispatcher for read_resource tool
767
+ async def load_resource(uri: str) -> dict | str:
768
+ """
769
+ Load an MCP resource by URI.
770
+
771
+ This function is called by the read_resource tool to dispatch to
772
+ registered resource handlers. Supports both regular resources and
773
+ parameterized resource templates (e.g., rem://agents/{agent_name}).
774
+
775
+ Args:
776
+ uri: Resource URI (e.g., "rem://agents", "rem://agents/ask_rem", "rem://status")
777
+
778
+ Returns:
779
+ Resource data (dict or string)
780
+
781
+ Raises:
782
+ ValueError: If URI is invalid or resource not found
783
+ """
784
+ import inspect
785
+ from fastmcp import FastMCP
786
+
787
+ # Create temporary MCP instance with resources
788
+ mcp = FastMCP(name="temp")
789
+
790
+ # Register all resources
791
+ register_schema_resources(mcp)
792
+ register_agent_resources(mcp)
793
+ register_file_resources(mcp)
794
+ register_status_resources(mcp)
795
+ register_session_resources(mcp)
796
+ register_user_resources(mcp)
797
+
798
+ # 1. Try exact match in regular resources
799
+ resources = await mcp.get_resources()
800
+ if uri in resources:
801
+ resource = resources[uri]
802
+ result = resource.fn()
803
+ if inspect.iscoroutine(result):
804
+ result = await result
805
+ return result if result else {"error": "Resource returned None"}
806
+
807
+ # 2. Try matching against parameterized resource templates
808
+ templates = await mcp.get_resource_templates()
809
+ for template_uri, template in templates.items():
810
+ params = template.matches(uri)
811
+ if params is not None:
812
+ # Template matched - call function with extracted parameters
813
+ result = template.fn(**params)
814
+ if inspect.iscoroutine(result):
815
+ result = await result
816
+ return result if result else {"error": "Resource returned None"}
817
+
818
+ # 3. Not found - include both resources and templates in error
819
+ available = list(resources.keys()) + list(templates.keys())
820
+ raise ValueError(f"Resource not found: {uri}. Available resources: {available}")