remdb 0.2.6__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 (187) hide show
  1. rem/__init__.py +2 -0
  2. rem/agentic/README.md +650 -0
  3. rem/agentic/__init__.py +39 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +8 -0
  6. rem/agentic/context.py +148 -0
  7. rem/agentic/context_builder.py +329 -0
  8. rem/agentic/mcp/__init__.py +0 -0
  9. rem/agentic/mcp/tool_wrapper.py +107 -0
  10. rem/agentic/otel/__init__.py +5 -0
  11. rem/agentic/otel/setup.py +151 -0
  12. rem/agentic/providers/phoenix.py +674 -0
  13. rem/agentic/providers/pydantic_ai.py +572 -0
  14. rem/agentic/query.py +117 -0
  15. rem/agentic/query_helper.py +89 -0
  16. rem/agentic/schema.py +396 -0
  17. rem/agentic/serialization.py +245 -0
  18. rem/agentic/tools/__init__.py +5 -0
  19. rem/agentic/tools/rem_tools.py +231 -0
  20. rem/api/README.md +420 -0
  21. rem/api/main.py +324 -0
  22. rem/api/mcp_router/prompts.py +182 -0
  23. rem/api/mcp_router/resources.py +536 -0
  24. rem/api/mcp_router/server.py +213 -0
  25. rem/api/mcp_router/tools.py +584 -0
  26. rem/api/routers/auth.py +229 -0
  27. rem/api/routers/chat/__init__.py +5 -0
  28. rem/api/routers/chat/completions.py +281 -0
  29. rem/api/routers/chat/json_utils.py +76 -0
  30. rem/api/routers/chat/models.py +124 -0
  31. rem/api/routers/chat/streaming.py +185 -0
  32. rem/auth/README.md +258 -0
  33. rem/auth/__init__.py +26 -0
  34. rem/auth/middleware.py +100 -0
  35. rem/auth/providers/__init__.py +13 -0
  36. rem/auth/providers/base.py +376 -0
  37. rem/auth/providers/google.py +163 -0
  38. rem/auth/providers/microsoft.py +237 -0
  39. rem/cli/README.md +455 -0
  40. rem/cli/__init__.py +8 -0
  41. rem/cli/commands/README.md +126 -0
  42. rem/cli/commands/__init__.py +3 -0
  43. rem/cli/commands/ask.py +565 -0
  44. rem/cli/commands/configure.py +423 -0
  45. rem/cli/commands/db.py +493 -0
  46. rem/cli/commands/dreaming.py +324 -0
  47. rem/cli/commands/experiments.py +1124 -0
  48. rem/cli/commands/mcp.py +66 -0
  49. rem/cli/commands/process.py +245 -0
  50. rem/cli/commands/schema.py +183 -0
  51. rem/cli/commands/serve.py +106 -0
  52. rem/cli/dreaming.py +363 -0
  53. rem/cli/main.py +88 -0
  54. rem/config.py +237 -0
  55. rem/mcp_server.py +41 -0
  56. rem/models/core/__init__.py +49 -0
  57. rem/models/core/core_model.py +64 -0
  58. rem/models/core/engram.py +333 -0
  59. rem/models/core/experiment.py +628 -0
  60. rem/models/core/inline_edge.py +132 -0
  61. rem/models/core/rem_query.py +243 -0
  62. rem/models/entities/__init__.py +43 -0
  63. rem/models/entities/file.py +57 -0
  64. rem/models/entities/image_resource.py +88 -0
  65. rem/models/entities/message.py +35 -0
  66. rem/models/entities/moment.py +123 -0
  67. rem/models/entities/ontology.py +191 -0
  68. rem/models/entities/ontology_config.py +131 -0
  69. rem/models/entities/resource.py +95 -0
  70. rem/models/entities/schema.py +87 -0
  71. rem/models/entities/user.py +85 -0
  72. rem/py.typed +0 -0
  73. rem/schemas/README.md +507 -0
  74. rem/schemas/__init__.py +6 -0
  75. rem/schemas/agents/README.md +92 -0
  76. rem/schemas/agents/core/moment-builder.yaml +178 -0
  77. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  78. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  79. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  80. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  81. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  82. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  83. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  84. rem/schemas/agents/examples/hello-world.yaml +37 -0
  85. rem/schemas/agents/examples/query.yaml +54 -0
  86. rem/schemas/agents/examples/simple.yaml +21 -0
  87. rem/schemas/agents/examples/test.yaml +29 -0
  88. rem/schemas/agents/rem.yaml +128 -0
  89. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  90. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  91. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  92. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  93. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  94. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  95. rem/services/__init__.py +16 -0
  96. rem/services/audio/INTEGRATION.md +308 -0
  97. rem/services/audio/README.md +376 -0
  98. rem/services/audio/__init__.py +15 -0
  99. rem/services/audio/chunker.py +354 -0
  100. rem/services/audio/transcriber.py +259 -0
  101. rem/services/content/README.md +1269 -0
  102. rem/services/content/__init__.py +5 -0
  103. rem/services/content/providers.py +806 -0
  104. rem/services/content/service.py +657 -0
  105. rem/services/dreaming/README.md +230 -0
  106. rem/services/dreaming/__init__.py +53 -0
  107. rem/services/dreaming/affinity_service.py +336 -0
  108. rem/services/dreaming/moment_service.py +264 -0
  109. rem/services/dreaming/ontology_service.py +54 -0
  110. rem/services/dreaming/user_model_service.py +297 -0
  111. rem/services/dreaming/utils.py +39 -0
  112. rem/services/embeddings/__init__.py +11 -0
  113. rem/services/embeddings/api.py +120 -0
  114. rem/services/embeddings/worker.py +421 -0
  115. rem/services/fs/README.md +662 -0
  116. rem/services/fs/__init__.py +62 -0
  117. rem/services/fs/examples.py +206 -0
  118. rem/services/fs/examples_paths.py +204 -0
  119. rem/services/fs/git_provider.py +935 -0
  120. rem/services/fs/local_provider.py +760 -0
  121. rem/services/fs/parsing-hooks-examples.md +172 -0
  122. rem/services/fs/paths.py +276 -0
  123. rem/services/fs/provider.py +460 -0
  124. rem/services/fs/s3_provider.py +1042 -0
  125. rem/services/fs/service.py +186 -0
  126. rem/services/git/README.md +1075 -0
  127. rem/services/git/__init__.py +17 -0
  128. rem/services/git/service.py +469 -0
  129. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  130. rem/services/phoenix/README.md +453 -0
  131. rem/services/phoenix/__init__.py +46 -0
  132. rem/services/phoenix/client.py +686 -0
  133. rem/services/phoenix/config.py +88 -0
  134. rem/services/phoenix/prompt_labels.py +477 -0
  135. rem/services/postgres/README.md +575 -0
  136. rem/services/postgres/__init__.py +23 -0
  137. rem/services/postgres/migration_service.py +427 -0
  138. rem/services/postgres/pydantic_to_sqlalchemy.py +232 -0
  139. rem/services/postgres/register_type.py +352 -0
  140. rem/services/postgres/repository.py +337 -0
  141. rem/services/postgres/schema_generator.py +379 -0
  142. rem/services/postgres/service.py +802 -0
  143. rem/services/postgres/sql_builder.py +354 -0
  144. rem/services/rem/README.md +304 -0
  145. rem/services/rem/__init__.py +23 -0
  146. rem/services/rem/exceptions.py +71 -0
  147. rem/services/rem/executor.py +293 -0
  148. rem/services/rem/parser.py +145 -0
  149. rem/services/rem/queries.py +196 -0
  150. rem/services/rem/query.py +371 -0
  151. rem/services/rem/service.py +527 -0
  152. rem/services/session/README.md +374 -0
  153. rem/services/session/__init__.py +6 -0
  154. rem/services/session/compression.py +360 -0
  155. rem/services/session/reload.py +77 -0
  156. rem/settings.py +1235 -0
  157. rem/sql/002_install_models.sql +1068 -0
  158. rem/sql/background_indexes.sql +42 -0
  159. rem/sql/install_models.sql +1038 -0
  160. rem/sql/migrations/001_install.sql +503 -0
  161. rem/sql/migrations/002_install_models.sql +1202 -0
  162. rem/utils/AGENTIC_CHUNKING.md +597 -0
  163. rem/utils/README.md +583 -0
  164. rem/utils/__init__.py +43 -0
  165. rem/utils/agentic_chunking.py +622 -0
  166. rem/utils/batch_ops.py +343 -0
  167. rem/utils/chunking.py +108 -0
  168. rem/utils/clip_embeddings.py +276 -0
  169. rem/utils/dict_utils.py +98 -0
  170. rem/utils/embeddings.py +423 -0
  171. rem/utils/examples/embeddings_example.py +305 -0
  172. rem/utils/examples/sql_types_example.py +202 -0
  173. rem/utils/markdown.py +16 -0
  174. rem/utils/model_helpers.py +236 -0
  175. rem/utils/schema_loader.py +229 -0
  176. rem/utils/sql_types.py +348 -0
  177. rem/utils/user_id.py +81 -0
  178. rem/utils/vision.py +330 -0
  179. rem/workers/README.md +506 -0
  180. rem/workers/__init__.py +5 -0
  181. rem/workers/dreaming.py +502 -0
  182. rem/workers/engram_processor.py +312 -0
  183. rem/workers/sqs_file_processor.py +193 -0
  184. remdb-0.2.6.dist-info/METADATA +1191 -0
  185. remdb-0.2.6.dist-info/RECORD +187 -0
  186. remdb-0.2.6.dist-info/WHEEL +4 -0
  187. remdb-0.2.6.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,584 @@
1
+ """
2
+ MCP Tools for REM operations.
3
+
4
+ Tools are functions that LLMs can call to interact with the REM system.
5
+ Each tool is decorated with @mcp.tool() and registered with the FastMCP server.
6
+
7
+ Design Pattern:
8
+ - Tools receive parameters from LLM
9
+ - Tools delegate to RemService or ContentService
10
+ - Tools return structured results
11
+ - Tools handle errors gracefully with informative messages
12
+
13
+ Available Tools:
14
+ - search_rem: Execute REM queries (LOOKUP, FUZZY, SEARCH, SQL, TRAVERSE)
15
+ - ask_rem_agent: Natural language to REM query conversion via agent
16
+ - ingest_into_rem: Full file ingestion pipeline (read + store + parse + chunk)
17
+ - read_resource: Access MCP resources (for Claude Desktop compatibility)
18
+ """
19
+
20
+ from functools import wraps
21
+ from typing import Any, Callable, Literal, cast
22
+
23
+ from loguru import logger
24
+
25
+ from ...agentic.context import AgentContext
26
+ from ...models.core import (
27
+ FuzzyParameters,
28
+ LookupParameters,
29
+ QueryType,
30
+ RemQuery,
31
+ SearchParameters,
32
+ SQLParameters,
33
+ TraverseParameters,
34
+ )
35
+ from ...services.postgres import PostgresService
36
+ from ...services.rem import RemService
37
+ from ...settings import settings
38
+
39
+
40
+ # Service cache for FastAPI lifespan initialization
41
+ _service_cache: dict[str, Any] = {}
42
+
43
+
44
+ def init_services(postgres_service: PostgresService, rem_service: RemService):
45
+ """
46
+ Initialize service instances for MCP tools.
47
+
48
+ Called during FastAPI lifespan startup.
49
+
50
+ Args:
51
+ postgres_service: PostgresService instance
52
+ rem_service: RemService instance
53
+ """
54
+ _service_cache["postgres"] = postgres_service
55
+ _service_cache["rem"] = rem_service
56
+ logger.info("MCP tools initialized with service instances")
57
+
58
+
59
+ async def get_rem_service() -> RemService:
60
+ """
61
+ Get or create RemService instance (lazy initialization).
62
+
63
+ Returns cached instance if available, otherwise creates new one.
64
+ Thread-safe for async usage.
65
+ """
66
+ if "rem" in _service_cache:
67
+ return cast(RemService, _service_cache["rem"])
68
+
69
+ # Lazy initialization for in-process/CLI usage
70
+ from ...services.postgres import get_postgres_service
71
+
72
+ postgres_service = get_postgres_service()
73
+ if not postgres_service:
74
+ raise RuntimeError("PostgreSQL is disabled. Cannot use REM service.")
75
+
76
+ await postgres_service.connect()
77
+ rem_service = RemService(postgres_service=postgres_service)
78
+
79
+ _service_cache["postgres"] = postgres_service
80
+ _service_cache["rem"] = rem_service
81
+
82
+ logger.info("MCP tools: lazy initialized services")
83
+ return rem_service
84
+
85
+
86
+ def mcp_tool_error_handler(func: Callable) -> Callable:
87
+ """
88
+ Decorator for consistent MCP tool error handling.
89
+
90
+ Wraps tool functions to:
91
+ - Log errors with full context
92
+ - Return standardized error responses
93
+ - Prevent exceptions from bubbling to LLM
94
+
95
+ Usage:
96
+ @mcp_tool_error_handler
97
+ async def my_tool(...) -> dict[str, Any]:
98
+ # Pure business logic - no try/except needed
99
+ result = await service.do_work()
100
+ return {"data": result}
101
+
102
+ Returns:
103
+ {"status": "success", **result} on success
104
+ {"status": "error", "error": str(e)} on failure
105
+ """
106
+ @wraps(func)
107
+ async def wrapper(*args, **kwargs) -> dict[str, Any]:
108
+ try:
109
+ result = await func(*args, **kwargs)
110
+ # If result already has status, return as-is
111
+ if isinstance(result, dict) and "status" in result:
112
+ return result
113
+ # Otherwise wrap in success response
114
+ return {"status": "success", **result}
115
+ except Exception as e:
116
+ logger.error(f"{func.__name__} failed: {e}", exc_info=True)
117
+ return {
118
+ "status": "error",
119
+ "error": str(e),
120
+ "tool": func.__name__,
121
+ }
122
+ return wrapper
123
+
124
+
125
+ @mcp_tool_error_handler
126
+ async def search_rem(
127
+ query_type: Literal["lookup", "fuzzy", "search", "sql", "traverse"],
128
+ # LOOKUP parameters
129
+ entity_key: str | None = None,
130
+ # FUZZY parameters
131
+ query_text: str | None = None,
132
+ threshold: float = 0.7,
133
+ # SEARCH parameters
134
+ table: str | None = None,
135
+ limit: int = 20,
136
+ # SQL parameters
137
+ sql_query: str | None = None,
138
+ # TRAVERSE parameters
139
+ initial_query: str | None = None,
140
+ edge_types: list[str] | None = None,
141
+ depth: int = 1,
142
+ # Optional context override (defaults to authenticated user)
143
+ user_id: str | None = None,
144
+ ) -> dict[str, Any]:
145
+ """
146
+ Execute REM queries for entity lookup, semantic search, and graph traversal.
147
+
148
+ REM supports multiple query types for different retrieval patterns:
149
+
150
+ **LOOKUP** - O(1) entity resolution by natural language key:
151
+ - Fast exact match across all tables
152
+ - Uses indexed label_vector for instant retrieval
153
+ - Example: LOOKUP "Sarah Chen" returns all entities named "Sarah Chen"
154
+
155
+ **FUZZY** - Fuzzy text matching with similarity threshold:
156
+ - Finds partial matches and typos
157
+ - Example: FUZZY "sara" threshold=0.7 finds "Sarah Chen", "Sara Martinez"
158
+
159
+ **SEARCH** - Semantic vector search (table-specific):
160
+ - Finds conceptually similar entities
161
+ - Example: SEARCH "database migration" table=resources returns related documents
162
+
163
+ **SQL** - Direct SQL queries for structured data:
164
+ - Full PostgreSQL query power (scoped to table)
165
+ - Example: SQL "role = 'engineer'" (WHERE clause only)
166
+
167
+ **TRAVERSE** - Graph traversal following relationships:
168
+ - Explores entity neighborhood via graph edges
169
+ - Supports depth control and edge type filtering
170
+ - Example: TRAVERSE "Sarah Chen" edge_types=["manages", "reports_to"] depth=2
171
+
172
+ Args:
173
+ query_type: Type of query (lookup, fuzzy, search, sql, traverse)
174
+ entity_key: Entity key for LOOKUP (e.g., "Sarah Chen")
175
+ query_text: Search text for FUZZY or SEARCH
176
+ threshold: Similarity threshold for FUZZY (0.0-1.0)
177
+ table: Target table for SEARCH (resources, moments, users, etc.)
178
+ limit: Max results for SEARCH
179
+ sql_query: SQL WHERE clause for SQL type (e.g. "id = '123'")
180
+ initial_query: Starting entity for TRAVERSE
181
+ edge_types: Edge types to follow for TRAVERSE (e.g., ["manages", "reports_to"])
182
+ depth: Traversal depth for TRAVERSE (0=plan only, 1-5=actual traversal)
183
+ user_id: Optional user identifier (defaults to authenticated user or "default")
184
+
185
+ Returns:
186
+ Dict with query results, metadata, and execution info
187
+
188
+ Examples:
189
+ # Lookup entity (uses authenticated user context)
190
+ search_rem(
191
+ query_type="lookup",
192
+ entity_key="Sarah Chen"
193
+ )
194
+
195
+ # Semantic search
196
+ search_rem(
197
+ query_type="search",
198
+ query_text="database migration",
199
+ table="resources",
200
+ limit=10
201
+ )
202
+
203
+ # SQL query (WHERE clause only)
204
+ search_rem(
205
+ query_type="sql",
206
+ table="resources",
207
+ sql_query="category = 'document'"
208
+ )
209
+
210
+ # Graph traversal
211
+ search_rem(
212
+ query_type="traverse",
213
+ initial_query="Sarah Chen",
214
+ edge_types=["manages", "reports_to"],
215
+ depth=2
216
+ )
217
+ """
218
+ # Get RemService instance (lazy initialization)
219
+ rem_service = await get_rem_service()
220
+
221
+ # Get user_id from context if not provided
222
+ # TODO: Extract from authenticated session context when auth is enabled
223
+ user_id = AgentContext.get_user_id_or_default(user_id, source="search_rem")
224
+
225
+ # Normalize query_type to lowercase for case-insensitive REM dialect
226
+ query_type = cast(Literal["lookup", "fuzzy", "search", "sql", "traverse"], query_type.lower())
227
+
228
+ # Build RemQuery based on query_type
229
+ if query_type == "lookup":
230
+ if not entity_key:
231
+ return {"status": "error", "error": "entity_key required for LOOKUP"}
232
+
233
+ query = RemQuery(
234
+ query_type=QueryType.LOOKUP,
235
+ parameters=LookupParameters(
236
+ key=entity_key,
237
+ user_id=user_id,
238
+ ),
239
+ user_id=user_id,
240
+ )
241
+
242
+ elif query_type == "fuzzy":
243
+ if not query_text:
244
+ return {"status": "error", "error": "query_text required for FUZZY"}
245
+
246
+ query = RemQuery(
247
+ query_type=QueryType.FUZZY,
248
+ parameters=FuzzyParameters(
249
+ query_text=query_text,
250
+ threshold=threshold,
251
+ limit=limit, # Limit was missing in original logic but likely intended
252
+ ),
253
+ user_id=user_id,
254
+ )
255
+
256
+ elif query_type == "search":
257
+ if not query_text:
258
+ return {"status": "error", "error": "query_text required for SEARCH"}
259
+ if not table:
260
+ return {"status": "error", "error": "table required for SEARCH"}
261
+
262
+ query = RemQuery(
263
+ query_type=QueryType.SEARCH,
264
+ parameters=SearchParameters(
265
+ query_text=query_text,
266
+ table_name=table,
267
+ limit=limit,
268
+ ),
269
+ user_id=user_id,
270
+ )
271
+
272
+ elif query_type == "sql":
273
+ if not sql_query:
274
+ return {"status": "error", "error": "sql_query required for SQL"}
275
+
276
+ # SQLParameters requires table_name. If not provided, we cannot execute.
277
+ # Assuming sql_query is just the WHERE clause based on RemService implementation,
278
+ # OR if table is provided we use it.
279
+ if not table:
280
+ return {"status": "error", "error": "table required for SQL queries (parameter: table)"}
281
+
282
+ query = RemQuery(
283
+ query_type=QueryType.SQL,
284
+ parameters=SQLParameters(
285
+ table_name=table,
286
+ where_clause=sql_query,
287
+ limit=limit,
288
+ ),
289
+ user_id=user_id,
290
+ )
291
+
292
+ elif query_type == "traverse":
293
+ if not initial_query:
294
+ return {
295
+ "status": "error",
296
+ "error": "initial_query required for TRAVERSE",
297
+ }
298
+
299
+ query = RemQuery(
300
+ query_type=QueryType.TRAVERSE,
301
+ parameters=TraverseParameters(
302
+ initial_query=initial_query,
303
+ edge_types=edge_types or [],
304
+ max_depth=depth,
305
+ ),
306
+ user_id=user_id,
307
+ )
308
+
309
+ else:
310
+ return {"status": "error", "error": f"Unknown query_type: {query_type}"}
311
+
312
+ # Execute query (errors handled by decorator)
313
+ logger.info(f"Executing REM query: {query_type} for user {user_id}")
314
+ result = await rem_service.execute_query(query)
315
+
316
+ logger.info(f"Query completed successfully: {query_type}")
317
+ return {
318
+ "query_type": query_type,
319
+ "results": result,
320
+ }
321
+
322
+
323
+ @mcp_tool_error_handler
324
+ async def ask_rem_agent(
325
+ query: str,
326
+ agent_schema: str = "ask_rem",
327
+ agent_version: str | None = None,
328
+ user_id: str | None = None,
329
+ ) -> dict[str, Any]:
330
+ """
331
+ Ask REM using natural language via agent-driven query conversion.
332
+
333
+ This tool converts natural language questions into optimized REM queries
334
+ using an agent that understands the REM query language and schema.
335
+
336
+ The agent can perform multi-turn reasoning and iterated retrieval:
337
+ 1. Initial exploration (LOOKUP/FUZZY to find entities)
338
+ 2. Semantic search (SEARCH for related content)
339
+ 3. Graph traversal (TRAVERSE to explore relationships)
340
+ 4. Synthesis (combine results into final answer)
341
+
342
+ Args:
343
+ query: Natural language question or task
344
+ agent_schema: Agent schema name (default: "ask_rem")
345
+ agent_version: Optional agent version (default: latest)
346
+ user_id: Optional user identifier (defaults to authenticated user or "default")
347
+
348
+ Returns:
349
+ Dict with:
350
+ - status: "success" or "error"
351
+ - response: Agent's natural language response
352
+ - query_output: Structured query results (if available)
353
+ - queries_executed: List of REM queries executed
354
+ - metadata: Agent execution metadata
355
+
356
+ Examples:
357
+ # Simple question (uses authenticated user context)
358
+ ask_rem_agent(
359
+ query="Who is Sarah Chen?"
360
+ )
361
+
362
+ # Complex multi-step question
363
+ ask_rem_agent(
364
+ query="What are the key findings from last week's sprint retrospective?"
365
+ )
366
+
367
+ # Graph exploration
368
+ ask_rem_agent(
369
+ query="Show me Sarah's reporting chain and their recent projects"
370
+ )
371
+ """
372
+ # Get user_id from context if not provided
373
+ # TODO: Extract from authenticated session context when auth is enabled
374
+ user_id = AgentContext.get_user_id_or_default(user_id, source="ask_rem_agent")
375
+
376
+ from ...agentic import create_agent
377
+ from ...utils.schema_loader import load_agent_schema
378
+
379
+ # Create agent context
380
+ context = AgentContext(
381
+ user_id=user_id,
382
+ tenant_id=user_id, # Set tenant_id to user_id for backward compat
383
+ default_model=settings.llm.default_model,
384
+ )
385
+
386
+ # Load agent schema
387
+ try:
388
+ schema = load_agent_schema(agent_schema)
389
+ except FileNotFoundError:
390
+ return {
391
+ "status": "error",
392
+ "error": f"Agent schema not found: {agent_schema}",
393
+ }
394
+
395
+ # Create agent
396
+ agent_runtime = await create_agent(
397
+ context=context,
398
+ agent_schema_override=schema,
399
+ )
400
+
401
+ # Run agent (errors handled by decorator)
402
+ logger.info(f"Running ask_rem agent for query: {query[:100]}...")
403
+ result = await agent_runtime.run(query)
404
+
405
+ # Extract output
406
+ from rem.agentic.serialization import serialize_agent_result
407
+ query_output = serialize_agent_result(result.output)
408
+
409
+ logger.info("Agent execution completed successfully")
410
+
411
+ return {
412
+ "response": str(result.output),
413
+ "query_output": query_output,
414
+ "natural_query": query,
415
+ }
416
+
417
+
418
+ @mcp_tool_error_handler
419
+ async def ingest_into_rem(
420
+ file_uri: str,
421
+ category: str | None = None,
422
+ tags: list[str] | None = None,
423
+ is_local_server: bool = False,
424
+ user_id: str | None = None,
425
+ ) -> dict[str, Any]:
426
+ """
427
+ Ingest file into REM, creating searchable resources and embeddings.
428
+
429
+ This tool provides the complete file ingestion pipeline:
430
+ 1. **Read**: File from local/S3/HTTP
431
+ 2. **Store**: To user-scoped internal storage
432
+ 3. **Parse**: Extract content, metadata, tables, images
433
+ 4. **Chunk**: Semantic chunking for embeddings
434
+ 5. **Embed**: Create Resource chunks with vector embeddings
435
+
436
+ Supported file types:
437
+ - Documents: PDF, DOCX, TXT, Markdown
438
+ - Code: Python, JavaScript, TypeScript, etc.
439
+ - Data: CSV, JSON, YAML
440
+ - Audio: WAV, MP3 (transcription)
441
+
442
+ **Security**: Remote MCP servers cannot read local files. Only local/stdio
443
+ MCP servers can access local filesystem paths.
444
+
445
+ Args:
446
+ file_uri: File location (local path, s3:// URI, or http(s):// URL)
447
+ category: Optional category (document, code, audio, etc.)
448
+ tags: Optional tags for file
449
+ is_local_server: True if running as local/stdio MCP server
450
+ user_id: Optional user identifier (defaults to authenticated user or "default")
451
+
452
+ Returns:
453
+ Dict with:
454
+ - status: "success" or "error"
455
+ - file_id: Created file UUID
456
+ - file_name: Original filename
457
+ - storage_uri: Internal storage URI
458
+ - processing_status: "completed" or "failed"
459
+ - resources_created: Number of Resource chunks created
460
+ - content: Parsed file content (markdown format) if completed
461
+ - message: Human-readable status message
462
+
463
+ Examples:
464
+ # Ingest local file (local server only, uses authenticated user context)
465
+ ingest_into_rem(
466
+ file_uri="/Users/me/contract.pdf",
467
+ category="legal",
468
+ is_local_server=True
469
+ )
470
+
471
+ # Ingest from S3
472
+ ingest_into_rem(
473
+ file_uri="s3://bucket/docs/report.pdf"
474
+ )
475
+
476
+ # Ingest from HTTP
477
+ ingest_into_rem(
478
+ file_uri="https://example.com/whitepaper.pdf",
479
+ tags=["research", "whitepaper"]
480
+ )
481
+ """
482
+ from ...services.content import ContentService
483
+
484
+ # Get user_id from context if not provided
485
+ # TODO: Extract from authenticated session context when auth is enabled
486
+ user_id = AgentContext.get_user_id_or_default(user_id, source="ingest_into_rem")
487
+
488
+ # Delegate to ContentService for centralized ingestion (errors handled by decorator)
489
+ content_service = ContentService()
490
+ result = await content_service.ingest_file(
491
+ file_uri=file_uri,
492
+ user_id=user_id,
493
+ category=category,
494
+ tags=tags,
495
+ is_local_server=is_local_server,
496
+ )
497
+
498
+ logger.info(
499
+ f"MCP ingestion complete: {result['file_name']} "
500
+ f"(status: {result['processing_status']}, "
501
+ f"resources: {result['resources_created']})"
502
+ )
503
+
504
+ return result
505
+
506
+
507
+ @mcp_tool_error_handler
508
+ async def read_resource(uri: str) -> dict[str, Any]:
509
+ """
510
+ Read an MCP resource by URI.
511
+
512
+ This tool provides automatic access to MCP resources in Claude Desktop.
513
+ Resources contain authoritative, up-to-date reference data.
514
+
515
+ **IMPORTANT**: This tool enables Claude Desktop to automatically access
516
+ resources based on query relevance. While FastMCP correctly exposes resources
517
+ via standard MCP resource endpoints, Claude Desktop currently requires manual
518
+ resource attachment. This tool bridges that gap by exposing resource access
519
+ as a tool, which Claude Desktop WILL automatically invoke.
520
+
521
+ **Available Resources:**
522
+
523
+ Agent Schemas:
524
+ • rem://schemas - List all agent schemas
525
+ • rem://schema/{name} - Get specific schema definition
526
+ • rem://schema/{name}/{version} - Get specific version
527
+
528
+ System Status:
529
+ • rem://status - System health and statistics
530
+
531
+ Args:
532
+ uri: Resource URI (e.g., "rem://schemas", "rem://schema/ask_rem")
533
+
534
+ Returns:
535
+ Dict with:
536
+ - status: "success" or "error"
537
+ - uri: Original URI
538
+ - data: Resource data (format depends on resource type)
539
+
540
+ Examples:
541
+ # List all schemas
542
+ read_resource(uri="rem://schemas")
543
+
544
+ # Get specific schema
545
+ read_resource(uri="rem://schema/ask_rem")
546
+
547
+ # Get schema version
548
+ read_resource(uri="rem://schema/ask_rem/v1.0.0")
549
+
550
+ # Check system status
551
+ read_resource(uri="rem://status")
552
+ """
553
+ logger.info(f"📖 Reading resource: {uri}")
554
+
555
+ # Import here to avoid circular dependency
556
+ from .resources import load_resource
557
+
558
+ # Load resource using the existing resource handler (errors handled by decorator)
559
+ result = await load_resource(uri)
560
+
561
+ logger.info(f"✓ Resource loaded successfully: {uri}")
562
+
563
+ # If result is already a dict, return it
564
+ if isinstance(result, dict):
565
+ return {
566
+ "uri": uri,
567
+ "data": result,
568
+ }
569
+
570
+ # If result is a string (JSON), parse it
571
+ import json
572
+
573
+ try:
574
+ data = json.loads(result)
575
+ return {
576
+ "uri": uri,
577
+ "data": data,
578
+ }
579
+ except json.JSONDecodeError:
580
+ # Return as plain text if not JSON
581
+ return {
582
+ "uri": uri,
583
+ "data": {"content": result},
584
+ }