remdb 0.3.171__py3-none-any.whl → 0.3.230__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 (59) hide show
  1. rem/agentic/README.md +36 -2
  2. rem/agentic/context.py +173 -0
  3. rem/agentic/context_builder.py +12 -2
  4. rem/agentic/mcp/tool_wrapper.py +39 -16
  5. rem/agentic/providers/pydantic_ai.py +78 -45
  6. rem/agentic/schema.py +6 -5
  7. rem/agentic/tools/rem_tools.py +11 -0
  8. rem/api/main.py +1 -1
  9. rem/api/mcp_router/resources.py +75 -14
  10. rem/api/mcp_router/server.py +31 -24
  11. rem/api/mcp_router/tools.py +621 -166
  12. rem/api/routers/admin.py +30 -4
  13. rem/api/routers/auth.py +114 -15
  14. rem/api/routers/chat/child_streaming.py +379 -0
  15. rem/api/routers/chat/completions.py +74 -37
  16. rem/api/routers/chat/sse_events.py +7 -3
  17. rem/api/routers/chat/streaming.py +352 -257
  18. rem/api/routers/chat/streaming_utils.py +327 -0
  19. rem/api/routers/common.py +18 -0
  20. rem/api/routers/dev.py +7 -1
  21. rem/api/routers/feedback.py +9 -1
  22. rem/api/routers/messages.py +176 -38
  23. rem/api/routers/models.py +9 -1
  24. rem/api/routers/query.py +12 -1
  25. rem/api/routers/shared_sessions.py +16 -0
  26. rem/auth/jwt.py +19 -4
  27. rem/auth/middleware.py +42 -28
  28. rem/cli/README.md +62 -0
  29. rem/cli/commands/ask.py +61 -81
  30. rem/cli/commands/db.py +148 -70
  31. rem/cli/commands/process.py +171 -43
  32. rem/models/entities/ontology.py +91 -101
  33. rem/schemas/agents/rem.yaml +1 -1
  34. rem/services/content/service.py +18 -5
  35. rem/services/email/service.py +11 -2
  36. rem/services/embeddings/worker.py +26 -12
  37. rem/services/postgres/__init__.py +28 -3
  38. rem/services/postgres/diff_service.py +57 -5
  39. rem/services/postgres/programmable_diff_service.py +635 -0
  40. rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
  41. rem/services/postgres/register_type.py +12 -11
  42. rem/services/postgres/repository.py +39 -29
  43. rem/services/postgres/schema_generator.py +5 -5
  44. rem/services/postgres/sql_builder.py +6 -5
  45. rem/services/session/__init__.py +8 -1
  46. rem/services/session/compression.py +40 -2
  47. rem/services/session/pydantic_messages.py +292 -0
  48. rem/settings.py +34 -0
  49. rem/sql/background_indexes.sql +5 -0
  50. rem/sql/migrations/001_install.sql +157 -10
  51. rem/sql/migrations/002_install_models.sql +160 -132
  52. rem/sql/migrations/004_cache_system.sql +7 -275
  53. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  54. rem/utils/model_helpers.py +101 -0
  55. rem/utils/schema_loader.py +79 -51
  56. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/METADATA +2 -2
  57. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/RECORD +59 -53
  58. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/WHEEL +0 -0
  59. {remdb-0.3.171.dist-info → remdb-0.3.230.dist-info}/entry_points.txt +0 -0
rem/agentic/README.md CHANGED
@@ -716,11 +716,45 @@ curl -X POST http://localhost:8000/api/v1/chat/completions \
716
716
 
717
717
  See `rem/api/README.md` for full SSE event protocol documentation.
718
718
 
719
+ ## Multi-Agent Orchestration
720
+
721
+ Agents can delegate work to other agents via the `ask_agent` tool. This enables orchestrator patterns where a parent agent routes to specialists.
722
+
723
+ ### How It Works
724
+
725
+ 1. **Parent agent** calls `ask_agent(agent_name, input_text)`
726
+ 2. **Child agent** executes and streams its response
727
+ 3. **Child events** bubble up to parent via an event sink (asyncio.Queue in ContextVar)
728
+ 4. **All tool calls** are saved to the database for the session
729
+
730
+ ### Key Components
731
+
732
+ | Component | Location | Purpose |
733
+ |-----------|----------|---------|
734
+ | `ask_agent` tool | `mcp_router/tools.py` | Loads child agent, runs with streaming, pushes events to sink |
735
+ | Event sink | `context.py` | ContextVar holding asyncio.Queue for child→parent event flow |
736
+ | Streaming controller | `streaming.py` | Drains event sink, emits SSE events, saves to DB |
737
+
738
+ ### Event Types
739
+
740
+ Child agents emit events that the parent streams to the client:
741
+
742
+ - **`child_tool_start`**: Child is calling a tool (logged, streamed, saved to DB)
743
+ - **`child_content`**: Child's text response (streamed as SSE content delta)
744
+ - **`child_tool_result`**: Tool completed with result (metadata extraction)
745
+
746
+ ### Testing
747
+
748
+ Integration tests in `tests/integration/test_ask_agent_streaming.py` verify:
749
+ - Child content streams correctly to client
750
+ - Tool calls are persisted to database
751
+ - Multi-turn conversations save all messages
752
+
719
753
  ## Future Work
720
754
 
721
755
  - [ ] Phoenix evaluator integration
722
756
  - [ ] Agent schema registry (load schemas by URI)
723
757
  - [ ] Schema validation and versioning
724
- - [ ] Multi-turn conversation management
725
- - [ ] Agent composition (agents calling agents)
758
+ - [x] Multi-turn conversation management
759
+ - [x] Agent composition (agents calling agents)
726
760
  - [ ] Alternative provider implementations (if needed)
rem/agentic/context.py CHANGED
@@ -22,14 +22,153 @@ Key Design Pattern:
22
22
  - Enables session tracking across API, CLI, and test execution
23
23
  - Supports header-based configuration override (model, schema URI)
24
24
  - Clean separation: context (who/what) vs agent (how)
25
+
26
+ Multi-Agent Context Propagation:
27
+ - ContextVar (_current_agent_context) threads context through nested agent calls
28
+ - Parent context is automatically available to child agents via get_current_context()
29
+ - Use agent_context_scope() context manager for scoped context setting
30
+ - Child agents inherit user_id, tenant_id, session_id, is_eval from parent
25
31
  """
26
32
 
33
+ import asyncio
34
+ from contextlib import contextmanager
35
+ from contextvars import ContextVar
36
+ from typing import Any, Generator
37
+
27
38
  from loguru import logger
28
39
  from pydantic import BaseModel, Field
29
40
 
30
41
  from ..settings import settings
31
42
 
32
43
 
44
+ # Thread-local context for current agent execution
45
+ # This enables context propagation through nested agent calls (multi-agent)
46
+ _current_agent_context: ContextVar["AgentContext | None"] = ContextVar(
47
+ "current_agent_context", default=None
48
+ )
49
+
50
+ # Event sink for streaming child agent events to parent
51
+ # When set, child agents (via ask_agent) should push their events here
52
+ # for the parent's streaming loop to proxy to the client
53
+ _parent_event_sink: ContextVar["asyncio.Queue | None"] = ContextVar(
54
+ "parent_event_sink", default=None
55
+ )
56
+
57
+
58
+ def get_current_context() -> "AgentContext | None":
59
+ """
60
+ Get the current agent context from context var.
61
+
62
+ Used by MCP tools (like ask_agent) to inherit context from parent agent.
63
+ Returns None if no context is set (e.g., direct CLI invocation without context).
64
+
65
+ Example:
66
+ # In an MCP tool
67
+ parent_context = get_current_context()
68
+ if parent_context:
69
+ # Inherit user_id, session_id, etc. from parent
70
+ child_context = parent_context.child_context(agent_schema_uri="child-agent")
71
+ """
72
+ return _current_agent_context.get()
73
+
74
+
75
+ def set_current_context(ctx: "AgentContext | None") -> None:
76
+ """
77
+ Set the current agent context.
78
+
79
+ Called by streaming layer before agent execution.
80
+ Should be cleared (set to None) after execution completes.
81
+ """
82
+ _current_agent_context.set(ctx)
83
+
84
+
85
+ @contextmanager
86
+ def agent_context_scope(ctx: "AgentContext") -> Generator["AgentContext", None, None]:
87
+ """
88
+ Context manager for scoped context setting.
89
+
90
+ Automatically restores previous context when exiting scope.
91
+ Safe for nested agent calls - each level preserves its parent's context.
92
+
93
+ Example:
94
+ context = AgentContext(user_id="user-123")
95
+ with agent_context_scope(context):
96
+ # Context is available via get_current_context()
97
+ result = await agent.run(...)
98
+ # Previous context (or None) is restored
99
+ """
100
+ previous = _current_agent_context.get()
101
+ _current_agent_context.set(ctx)
102
+ try:
103
+ yield ctx
104
+ finally:
105
+ _current_agent_context.set(previous)
106
+
107
+
108
+ # =============================================================================
109
+ # Event Sink for Streaming Multi-Agent Delegation
110
+ # =============================================================================
111
+
112
+
113
+ def get_event_sink() -> "asyncio.Queue | None":
114
+ """
115
+ Get the parent's event sink for streaming child events.
116
+
117
+ Used by ask_agent to push child agent events to the parent's stream.
118
+ Returns None if not in a streaming context.
119
+ """
120
+ return _parent_event_sink.get()
121
+
122
+
123
+ def set_event_sink(sink: "asyncio.Queue | None") -> None:
124
+ """Set the event sink for child agents to push events to."""
125
+ _parent_event_sink.set(sink)
126
+
127
+
128
+ @contextmanager
129
+ def event_sink_scope(sink: "asyncio.Queue") -> Generator["asyncio.Queue", None, None]:
130
+ """
131
+ Context manager for scoped event sink setting.
132
+
133
+ Used by streaming layer to set up event proxying before tool execution.
134
+ Child agents (via ask_agent) will push their events to this sink.
135
+
136
+ Example:
137
+ event_queue = asyncio.Queue()
138
+ with event_sink_scope(event_queue):
139
+ # ask_agent will push child events to event_queue
140
+ async for event in tools_stream:
141
+ ...
142
+ # Also consume from event_queue
143
+ """
144
+ previous = _parent_event_sink.get()
145
+ _parent_event_sink.set(sink)
146
+ try:
147
+ yield sink
148
+ finally:
149
+ _parent_event_sink.set(previous)
150
+
151
+
152
+ async def push_event(event: Any) -> bool:
153
+ """
154
+ Push an event to the parent's event sink (if available).
155
+
156
+ Used by ask_agent to proxy child agent events to the parent's stream.
157
+ Returns True if event was pushed, False if no sink available.
158
+
159
+ Args:
160
+ event: Any streaming event (ToolCallEvent, content chunk, etc.)
161
+
162
+ Returns:
163
+ True if event was pushed to sink, False otherwise
164
+ """
165
+ sink = _parent_event_sink.get()
166
+ if sink is not None:
167
+ await sink.put(event)
168
+ return True
169
+ return False
170
+
171
+
33
172
  class AgentContext(BaseModel):
34
173
  """
35
174
  Session and configuration context for agent execution.
@@ -85,6 +224,40 @@ class AgentContext(BaseModel):
85
224
 
86
225
  model_config = {"populate_by_name": True}
87
226
 
227
+ def child_context(
228
+ self,
229
+ agent_schema_uri: str | None = None,
230
+ model_override: str | None = None,
231
+ ) -> "AgentContext":
232
+ """
233
+ Create a child context for nested agent calls.
234
+
235
+ Inherits user_id, tenant_id, session_id, is_eval from parent.
236
+ Allows overriding agent_schema_uri and default_model for the child.
237
+
238
+ Args:
239
+ agent_schema_uri: Agent schema for the child agent (required for lineage)
240
+ model_override: Optional model override for child agent
241
+
242
+ Returns:
243
+ New AgentContext for the child agent
244
+
245
+ Example:
246
+ parent_context = get_current_context()
247
+ child_context = parent_context.child_context(
248
+ agent_schema_uri="sentiment-analyzer"
249
+ )
250
+ agent = await create_agent(context=child_context)
251
+ """
252
+ return AgentContext(
253
+ user_id=self.user_id,
254
+ tenant_id=self.tenant_id,
255
+ session_id=self.session_id,
256
+ default_model=model_override or self.default_model,
257
+ agent_schema_uri=agent_schema_uri or self.agent_schema_uri,
258
+ is_eval=self.is_eval,
259
+ )
260
+
88
261
  @staticmethod
89
262
  def get_user_id_or_default(
90
263
  user_id: str | None,
@@ -217,11 +217,21 @@ class ContextBuilder:
217
217
  )
218
218
 
219
219
  # Convert to ContextMessage format
220
+ # For tool messages, wrap content with clear markers so the agent
221
+ # can see previous tool results when the prompt is concatenated
220
222
  for msg_dict in session_history:
223
+ role = msg_dict["role"]
224
+ content = msg_dict["content"]
225
+
226
+ if role == "tool":
227
+ # Wrap tool results with clear markers for visibility
228
+ tool_name = msg_dict.get("tool_name", "unknown")
229
+ content = f"[TOOL RESULT: {tool_name}]\n{content}\n[/TOOL RESULT]"
230
+
221
231
  messages.append(
222
232
  ContextMessage(
223
- role=msg_dict["role"],
224
- content=msg_dict["content"],
233
+ role=role,
234
+ content=content,
225
235
  )
226
236
  )
227
237
 
@@ -116,7 +116,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
116
116
  the artificial MCP distinction between tools and resources.
117
117
 
118
118
  Supports both:
119
- - Concrete URIs: "rem://schemas" -> tool with no parameters
119
+ - Concrete URIs: "rem://agents" -> tool with no parameters
120
120
  - Template URIs: "patient-profile://field/{field_key}" -> tool with field_key parameter
121
121
 
122
122
  Args:
@@ -131,7 +131,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
131
131
 
132
132
  Example:
133
133
  # Concrete URI -> no-param tool
134
- tool = create_resource_tool("rem://schemas", "List all agent schemas")
134
+ tool = create_resource_tool("rem://agents", "List all agent schemas")
135
135
 
136
136
  # Template URI -> parameterized tool
137
137
  tool = create_resource_tool("patient-profile://field/{field_key}", "Get field definition", mcp_server=mcp)
@@ -161,6 +161,11 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
161
161
  param_desc = ", ".join(template_vars)
162
162
  description = f"{description}\n\nParameters: {param_desc}"
163
163
 
164
+ # Capture mcp_server reference at tool creation time (for closure)
165
+ # This ensures the correct server is used even if called later
166
+ _captured_mcp_server = mcp_server
167
+ _captured_uri = uri # Also capture URI for consistent logging
168
+
164
169
  if template_vars:
165
170
  # Template URI -> create parameterized tool
166
171
  async def wrapper(**kwargs: Any) -> str:
@@ -168,13 +173,17 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
168
173
  import asyncio
169
174
  import inspect
170
175
 
176
+ logger.debug(f"Resource tool invoked: uri={_captured_uri}, kwargs={kwargs}, mcp_server={'set' if _captured_mcp_server else 'None'}")
177
+
171
178
  # Try to resolve from MCP server's resource templates first
172
- if mcp_server is not None:
179
+ if _captured_mcp_server is not None:
173
180
  try:
174
181
  # Get resource templates from MCP server
175
- templates = await mcp_server.get_resource_templates()
176
- if uri in templates:
177
- template = templates[uri]
182
+ templates = await _captured_mcp_server.get_resource_templates()
183
+ logger.debug(f"MCP server templates: {list(templates.keys())}")
184
+ if _captured_uri in templates:
185
+ template = templates[_captured_uri]
186
+ logger.debug(f"Found template for {_captured_uri}, calling fn with kwargs={kwargs}")
178
187
  # Call the template's underlying function directly
179
188
  # The fn expects the template variables as kwargs
180
189
  fn_result = template.fn(**kwargs)
@@ -184,17 +193,22 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
184
193
  if isinstance(fn_result, str):
185
194
  return fn_result
186
195
  return json.dumps(fn_result, indent=2)
196
+ else:
197
+ logger.warning(f"Template {_captured_uri} not found in MCP server templates: {list(templates.keys())}")
187
198
  except Exception as e:
188
- logger.warning(f"Failed to resolve resource {uri} from MCP server: {e}")
199
+ logger.warning(f"Failed to resolve resource {_captured_uri} from MCP server: {e}", exc_info=True)
200
+ else:
201
+ logger.warning(f"No MCP server provided for resource tool {_captured_uri}, using fallback")
189
202
 
190
203
  # Fallback: substitute template variables and use load_resource
191
- resolved_uri = uri
204
+ resolved_uri = _captured_uri
192
205
  for var in template_vars:
193
206
  if var in kwargs:
194
207
  resolved_uri = resolved_uri.replace(f"{{{var}}}", str(kwargs[var]))
195
208
  else:
196
209
  return json.dumps({"error": f"Missing required parameter: {var}"})
197
210
 
211
+ logger.debug(f"Using fallback load_resource for resolved URI: {resolved_uri}")
198
212
  from rem.api.mcp_router.resources import load_resource
199
213
  result = await load_resource(resolved_uri)
200
214
  if isinstance(result, str):
@@ -208,7 +222,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
208
222
  wrapper.__annotations__ = {var: str for var in template_vars}
209
223
  wrapper.__annotations__['return'] = str
210
224
 
211
- logger.info(f"Built parameterized resource tool: {func_name} (uri: {uri}, params: {template_vars})")
225
+ logger.info(f"Built parameterized resource tool: {func_name} (uri: {uri}, params: {template_vars}, mcp_server={'provided' if mcp_server else 'None'})")
212
226
  else:
213
227
  # Concrete URI -> no-param tool
214
228
  async def wrapper(**kwargs: Any) -> str:
@@ -219,12 +233,16 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
219
233
  if kwargs:
220
234
  logger.warning(f"Resource tool {func_name} called with unexpected kwargs: {list(kwargs.keys())}")
221
235
 
236
+ logger.debug(f"Concrete resource tool invoked: uri={_captured_uri}, mcp_server={'set' if _captured_mcp_server else 'None'}")
237
+
222
238
  # Try to resolve from MCP server's resources first
223
- if mcp_server is not None:
239
+ if _captured_mcp_server is not None:
224
240
  try:
225
- resources = await mcp_server.get_resources()
226
- if uri in resources:
227
- resource = resources[uri]
241
+ resources = await _captured_mcp_server.get_resources()
242
+ logger.debug(f"MCP server resources: {list(resources.keys())}")
243
+ if _captured_uri in resources:
244
+ resource = resources[_captured_uri]
245
+ logger.debug(f"Found resource for {_captured_uri}")
228
246
  # Call the resource's underlying function
229
247
  fn_result = resource.fn()
230
248
  if inspect.iscoroutine(fn_result):
@@ -232,12 +250,17 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
232
250
  if isinstance(fn_result, str):
233
251
  return fn_result
234
252
  return json.dumps(fn_result, indent=2)
253
+ else:
254
+ logger.warning(f"Resource {_captured_uri} not found in MCP server resources: {list(resources.keys())}")
235
255
  except Exception as e:
236
- logger.warning(f"Failed to resolve resource {uri} from MCP server: {e}")
256
+ logger.warning(f"Failed to resolve resource {_captured_uri} from MCP server: {e}", exc_info=True)
257
+ else:
258
+ logger.warning(f"No MCP server provided for resource tool {_captured_uri}, using fallback")
237
259
 
238
260
  # Fallback to load_resource
261
+ logger.debug(f"Using fallback load_resource for URI: {_captured_uri}")
239
262
  from rem.api.mcp_router.resources import load_resource
240
- result = await load_resource(uri)
263
+ result = await load_resource(_captured_uri)
241
264
  if isinstance(result, str):
242
265
  return result
243
266
  return json.dumps(result, indent=2)
@@ -245,6 +268,6 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
245
268
  wrapper.__name__ = func_name
246
269
  wrapper.__doc__ = description
247
270
 
248
- logger.info(f"Built resource tool: {func_name} (uri: {uri})")
271
+ logger.info(f"Built resource tool: {func_name} (uri: {uri}, mcp_server={'provided' if mcp_server else 'None'})")
249
272
 
250
273
  return Tool(wrapper)
@@ -96,6 +96,35 @@ TODO:
96
96
 
97
97
  Priority: HIGH (blocks production scaling beyond 50 req/sec)
98
98
 
99
+ 4. Response Format Control (structured_output enhancement):
100
+ - Current: structured_output is bool (True=strict schema, False=free-form text)
101
+ - Missing: OpenAI JSON mode (valid JSON without strict schema enforcement)
102
+ - Missing: Completions API support (some models only support completions, not chat)
103
+
104
+ Proposed schema field values for `structured_output`:
105
+ - True (default): Strict structured output using provider's native schema support
106
+ - False: Free-form text response (properties converted to prompt guidance)
107
+ - "json": JSON mode - ensures valid JSON but no schema enforcement
108
+ (OpenAI: response_format={"type": "json_object"})
109
+ - "text": Explicit free-form text (alias for False)
110
+
111
+ Implementation:
112
+ a) Update AgentSchemaMetadata.structured_output type:
113
+ structured_output: bool | Literal["json", "text"] = True
114
+ b) In create_agent(), handle each mode:
115
+ - True: Use output_type with Pydantic model (current behavior)
116
+ - False/"text": Convert properties to prompt guidance (current behavior)
117
+ - "json": Use provider's JSON mode without strict schema
118
+ c) Provider-specific JSON mode:
119
+ - OpenAI: model_settings={"response_format": {"type": "json_object"}}
120
+ - Anthropic: Not supported natively, use prompt guidance
121
+ - Others: Fallback to prompt guidance with JSON instruction
122
+
123
+ Related: Some providers (Cerebras) have completions-only models where
124
+ structured output isn't available. Consider model capability detection.
125
+
126
+ Priority: MEDIUM (enables more flexible output control)
127
+
99
128
  Example Agent Schema:
100
129
  {
101
130
  "type": "object",
@@ -550,70 +579,73 @@ async def create_agent(
550
579
  # Extract schema fields using typed helpers
551
580
  from ..schema import get_system_prompt, get_metadata
552
581
 
553
- # Track whether mcp_servers was explicitly configured (even if empty)
554
- mcp_servers_explicitly_set = False
555
-
556
582
  if agent_schema:
557
583
  system_prompt = get_system_prompt(agent_schema)
558
584
  metadata = get_metadata(agent_schema)
559
- # Check if mcp_servers was explicitly set (could be empty list to disable)
560
- if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers is not None:
561
- mcp_server_configs = [s.model_dump() for s in metadata.mcp_servers]
562
- mcp_servers_explicitly_set = True
563
- else:
564
- mcp_server_configs = []
565
585
  resource_configs = metadata.resources if hasattr(metadata, 'resources') else []
566
586
 
587
+ # DEPRECATED: mcp_servers in agent schemas is ignored
588
+ # MCP servers are now always auto-detected at the application level
589
+ if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers:
590
+ logger.warning(
591
+ "DEPRECATED: mcp_servers in agent schema is ignored. "
592
+ "MCP servers are auto-detected from tools.mcp_server module. "
593
+ "Remove mcp_servers from your agent schema."
594
+ )
595
+
567
596
  if metadata.system_prompt:
568
597
  logger.debug("Using custom system_prompt from json_schema_extra")
569
598
  else:
570
599
  system_prompt = ""
571
600
  metadata = None
572
- mcp_server_configs = []
573
601
  resource_configs = []
574
602
 
575
- # Auto-detect local MCP server if not explicitly configured
576
- # This makes mcp_servers config optional - agents get tools automatically
577
- # But if mcp_servers: [] is explicitly set, respect that (no auto-detection)
578
- if not mcp_server_configs and not mcp_servers_explicitly_set:
579
- import importlib
580
- import os
581
- import sys
582
-
583
- # Ensure current working directory is in sys.path for local imports
584
- cwd = os.getcwd()
585
- if cwd not in sys.path:
586
- sys.path.insert(0, cwd)
587
-
588
- # Try common local MCP server module paths first
589
- auto_detect_modules = [
590
- "tools.mcp_server", # Convention: tools/mcp_server.py
591
- "mcp_server", # Alternative: mcp_server.py in root
592
- ]
593
- for module_path in auto_detect_modules:
594
- try:
595
- mcp_module = importlib.import_module(module_path)
596
- if hasattr(mcp_module, "mcp"):
597
- logger.info(f"Auto-detected local MCP server: {module_path}")
598
- mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
599
- break
600
- except ImportError:
601
- continue
602
-
603
- # Fall back to REM's default MCP server if no local server found
604
- if not mcp_server_configs:
605
- logger.debug("No local MCP server found, using REM default")
606
- mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
603
+ # Auto-detect MCP server at application level
604
+ # Convention: tools/mcp_server.py exports `mcp` FastMCP instance
605
+ # Falls back to REM's built-in MCP server if no local server found
606
+ import importlib
607
+ import os
608
+ import sys
609
+
610
+ # Ensure current working directory is in sys.path for local imports
611
+ cwd = os.getcwd()
612
+ if cwd not in sys.path:
613
+ sys.path.insert(0, cwd)
614
+
615
+ mcp_server_configs = []
616
+ auto_detect_modules = [
617
+ "tools.mcp_server", # Convention: tools/mcp_server.py
618
+ "mcp_server", # Alternative: mcp_server.py in root
619
+ ]
620
+ for module_path in auto_detect_modules:
621
+ try:
622
+ mcp_module = importlib.import_module(module_path)
623
+ if hasattr(mcp_module, "mcp"):
624
+ logger.info(f"Auto-detected local MCP server: {module_path}")
625
+ mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
626
+ break
627
+ except ImportError as e:
628
+ logger.debug(f"MCP server auto-detect: {module_path} not found ({e})")
629
+ continue
630
+ except Exception as e:
631
+ logger.warning(f"MCP server auto-detect: {module_path} failed to load: {e}")
632
+ continue
633
+
634
+ # Fall back to REM's default MCP server if no local server found
635
+ if not mcp_server_configs:
636
+ logger.info("No local MCP server found, using REM default (rem.mcp_server)")
637
+ mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
607
638
 
608
639
  # Extract temperature and max_iterations from schema metadata (with fallback to settings defaults)
609
640
  if metadata:
610
641
  temperature = metadata.override_temperature if metadata.override_temperature is not None else settings.llm.default_temperature
611
642
  max_iterations = metadata.override_max_iterations if metadata.override_max_iterations is not None else settings.llm.default_max_iterations
612
- use_structured_output = metadata.structured_output
643
+ # Use schema-level structured_output if set, otherwise fall back to global setting
644
+ use_structured_output = metadata.structured_output if metadata.structured_output is not None else settings.llm.default_structured_output
613
645
  else:
614
646
  temperature = settings.llm.default_temperature
615
647
  max_iterations = settings.llm.default_max_iterations
616
- use_structured_output = True
648
+ use_structured_output = settings.llm.default_structured_output
617
649
 
618
650
  # Build list of tools - start with built-in tools
619
651
  tools = _get_builtin_tools()
@@ -700,7 +732,7 @@ async def create_agent(
700
732
  # the artificial MCP distinction between tools and resources
701
733
  #
702
734
  # Supports both concrete and template URIs:
703
- # - Concrete: "rem://schemas" -> no-param tool
735
+ # - Concrete: "rem://agents" -> no-param tool
704
736
  # - Template: "patient-profile://field/{field_key}" -> tool with field_key param
705
737
  from ..mcp.tool_wrapper import create_resource_tool
706
738
 
@@ -736,6 +768,7 @@ async def create_agent(
736
768
 
737
769
  # Create tools from collected resource URIs
738
770
  # Pass the loaded MCP server so resources can be resolved from it
771
+ logger.info(f"Creating {len(resource_uris)} resource tools with mcp_server={'set' if loaded_mcp_server else 'None'}")
739
772
  for uri, usage in resource_uris:
740
773
  resource_tool = create_resource_tool(uri, usage, mcp_server=loaded_mcp_server)
741
774
  tools.append(resource_tool)
rem/agentic/schema.py CHANGED
@@ -79,7 +79,7 @@ class MCPResourceReference(BaseModel):
79
79
 
80
80
  Example (exact URI):
81
81
  {
82
- "uri": "rem://schemas",
82
+ "uri": "rem://agents",
83
83
  "name": "Agent Schemas",
84
84
  "description": "List all available agent schemas"
85
85
  }
@@ -96,7 +96,7 @@ class MCPResourceReference(BaseModel):
96
96
  default=None,
97
97
  description=(
98
98
  "Exact resource URI or URI with query parameters. "
99
- "Examples: 'rem://schemas', 'rem://resources?category=drug.*'"
99
+ "Examples: 'rem://agents', 'rem://resources?category=drug.*'"
100
100
  )
101
101
  )
102
102
 
@@ -215,12 +215,13 @@ class AgentSchemaMetadata(BaseModel):
215
215
  )
216
216
 
217
217
  # Structured output toggle
218
- structured_output: bool = Field(
219
- default=True,
218
+ structured_output: bool | None = Field(
219
+ default=None,
220
220
  description=(
221
221
  "Whether to enforce structured JSON output. "
222
222
  "When False, the agent produces free-form text and schema properties "
223
- "are converted to prompt guidance instead. Default: True (JSON output)."
223
+ "are converted to prompt guidance instead. "
224
+ "Default: None (uses LLM__DEFAULT_STRUCTURED_OUTPUT setting, which defaults to False)."
224
225
  ),
225
226
  )
226
227
 
@@ -3,6 +3,17 @@ REM tools for agent execution (CLI and API compatible).
3
3
 
4
4
  These tools work in both CLI and API contexts by initializing services on-demand.
5
5
  They wrap the service layer directly, not MCP tools.
6
+
7
+ Core tables (always available):
8
+ - resources: Documents, content chunks, artifacts
9
+ - moments: Temporal narratives extracted from resources (usually user-specific)
10
+ - ontologies: Domain entities with semantic links for further lookups (like a wiki)
11
+
12
+ Other tables (may vary by deployment):
13
+ - users, sessions, messages, files, schemas, feedbacks
14
+
15
+ Note: Not all tables are populated in all systems. Use FUZZY or SEARCH
16
+ to discover what data exists before assuming specific tables have content.
6
17
  """
7
18
 
8
19
  from typing import Any, Literal, cast
rem/api/main.py CHANGED
@@ -322,7 +322,7 @@ def create_app() -> FastAPI:
322
322
 
323
323
  app.add_middleware(
324
324
  AuthMiddleware,
325
- protected_paths=["/api/v1"],
325
+ protected_paths=["/api/v1", "/api/admin"],
326
326
  excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth", "/api/v1/slack"],
327
327
  # Allow anonymous when auth is disabled, otherwise use setting
328
328
  allow_anonymous=(not settings.auth.enabled) or settings.auth.allow_anonymous,