remdb 0.3.127__py3-none-any.whl → 0.3.172__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.
- rem/agentic/agents/__init__.py +16 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/context.py +81 -3
- rem/agentic/context_builder.py +36 -9
- rem/agentic/mcp/tool_wrapper.py +132 -15
- rem/agentic/providers/phoenix.py +371 -108
- rem/agentic/providers/pydantic_ai.py +163 -45
- rem/agentic/schema.py +8 -4
- rem/api/deps.py +3 -5
- rem/api/main.py +22 -3
- rem/api/mcp_router/resources.py +15 -10
- rem/api/mcp_router/server.py +2 -0
- rem/api/mcp_router/tools.py +94 -2
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/auth.py +349 -6
- rem/api/routers/chat/completions.py +5 -3
- rem/api/routers/chat/streaming.py +95 -22
- rem/api/routers/messages.py +24 -15
- rem/auth/__init__.py +13 -3
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +115 -10
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/configure.py +3 -4
- rem/cli/commands/experiments.py +226 -50
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +2 -0
- rem/models/core/experiment.py +58 -14
- rem/models/entities/__init__.py +4 -0
- rem/models/entities/ontology.py +1 -1
- rem/models/entities/ontology_config.py +1 -1
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
- rem/schemas/agents/examples/contract-extractor.yaml +1 -1
- rem/schemas/agents/examples/cv-parser.yaml +1 -1
- rem/services/__init__.py +3 -1
- rem/services/content/service.py +4 -3
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +513 -0
- rem/services/email/templates.py +360 -0
- rem/services/postgres/README.md +38 -0
- rem/services/postgres/diff_service.py +19 -3
- rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
- rem/services/postgres/repository.py +5 -4
- rem/services/session/compression.py +113 -50
- rem/services/session/reload.py +14 -7
- rem/services/user_service.py +41 -9
- rem/settings.py +292 -5
- rem/sql/migrations/001_install.sql +1 -1
- rem/sql/migrations/002_install_models.sql +91 -91
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/README.md +45 -0
- rem/utils/files.py +157 -1
- rem/utils/schema_loader.py +45 -7
- rem/utils/vision.py +1 -1
- {remdb-0.3.127.dist-info → remdb-0.3.172.dist-info}/METADATA +7 -5
- {remdb-0.3.127.dist-info → remdb-0.3.172.dist-info}/RECORD +62 -52
- {remdb-0.3.127.dist-info → remdb-0.3.172.dist-info}/WHEEL +0 -0
- {remdb-0.3.127.dist-info → remdb-0.3.172.dist-info}/entry_points.txt +0 -0
|
@@ -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,10 +579,18 @@ async def create_agent(
|
|
|
550
579
|
# Extract schema fields using typed helpers
|
|
551
580
|
from ..schema import get_system_prompt, get_metadata
|
|
552
581
|
|
|
582
|
+
# Track whether mcp_servers was explicitly configured (even if empty)
|
|
583
|
+
mcp_servers_explicitly_set = False
|
|
584
|
+
|
|
553
585
|
if agent_schema:
|
|
554
586
|
system_prompt = get_system_prompt(agent_schema)
|
|
555
587
|
metadata = get_metadata(agent_schema)
|
|
556
|
-
|
|
588
|
+
# Check if mcp_servers was explicitly set (could be empty list to disable)
|
|
589
|
+
if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers is not None:
|
|
590
|
+
mcp_server_configs = [s.model_dump() for s in metadata.mcp_servers]
|
|
591
|
+
mcp_servers_explicitly_set = True
|
|
592
|
+
else:
|
|
593
|
+
mcp_server_configs = []
|
|
557
594
|
resource_configs = metadata.resources if hasattr(metadata, 'resources') else []
|
|
558
595
|
|
|
559
596
|
if metadata.system_prompt:
|
|
@@ -564,15 +601,49 @@ async def create_agent(
|
|
|
564
601
|
mcp_server_configs = []
|
|
565
602
|
resource_configs = []
|
|
566
603
|
|
|
604
|
+
# Auto-detect local MCP server if not explicitly configured
|
|
605
|
+
# This makes mcp_servers config optional - agents get tools automatically
|
|
606
|
+
# But if mcp_servers: [] is explicitly set, respect that (no auto-detection)
|
|
607
|
+
if not mcp_server_configs and not mcp_servers_explicitly_set:
|
|
608
|
+
import importlib
|
|
609
|
+
import os
|
|
610
|
+
import sys
|
|
611
|
+
|
|
612
|
+
# Ensure current working directory is in sys.path for local imports
|
|
613
|
+
cwd = os.getcwd()
|
|
614
|
+
if cwd not in sys.path:
|
|
615
|
+
sys.path.insert(0, cwd)
|
|
616
|
+
|
|
617
|
+
# Try common local MCP server module paths first
|
|
618
|
+
auto_detect_modules = [
|
|
619
|
+
"tools.mcp_server", # Convention: tools/mcp_server.py
|
|
620
|
+
"mcp_server", # Alternative: mcp_server.py in root
|
|
621
|
+
]
|
|
622
|
+
for module_path in auto_detect_modules:
|
|
623
|
+
try:
|
|
624
|
+
mcp_module = importlib.import_module(module_path)
|
|
625
|
+
if hasattr(mcp_module, "mcp"):
|
|
626
|
+
logger.info(f"Auto-detected local MCP server: {module_path}")
|
|
627
|
+
mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
|
|
628
|
+
break
|
|
629
|
+
except ImportError:
|
|
630
|
+
continue
|
|
631
|
+
|
|
632
|
+
# Fall back to REM's default MCP server if no local server found
|
|
633
|
+
if not mcp_server_configs:
|
|
634
|
+
logger.debug("No local MCP server found, using REM default")
|
|
635
|
+
mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
|
|
636
|
+
|
|
567
637
|
# Extract temperature and max_iterations from schema metadata (with fallback to settings defaults)
|
|
568
638
|
if metadata:
|
|
569
639
|
temperature = metadata.override_temperature if metadata.override_temperature is not None else settings.llm.default_temperature
|
|
570
640
|
max_iterations = metadata.override_max_iterations if metadata.override_max_iterations is not None else settings.llm.default_max_iterations
|
|
571
|
-
|
|
641
|
+
# Use schema-level structured_output if set, otherwise fall back to global setting
|
|
642
|
+
use_structured_output = metadata.structured_output if metadata.structured_output is not None else settings.llm.default_structured_output
|
|
572
643
|
else:
|
|
573
644
|
temperature = settings.llm.default_temperature
|
|
574
645
|
max_iterations = settings.llm.default_max_iterations
|
|
575
|
-
use_structured_output =
|
|
646
|
+
use_structured_output = settings.llm.default_structured_output
|
|
576
647
|
|
|
577
648
|
# Build list of tools - start with built-in tools
|
|
578
649
|
tools = _get_builtin_tools()
|
|
@@ -608,50 +679,97 @@ async def create_agent(
|
|
|
608
679
|
search_rem_suffix += f"Example: `SEARCH \"your query\" FROM {default_table} LIMIT 10`"
|
|
609
680
|
|
|
610
681
|
# Add tools from MCP server (in-process, no subprocess)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
wrapped_tool = create_mcp_tool_wrapper(
|
|
637
|
-
tool_name,
|
|
638
|
-
tool_func,
|
|
639
|
-
user_id=context.user_id if context else None,
|
|
640
|
-
description_suffix=tool_suffix,
|
|
641
|
-
)
|
|
642
|
-
tools.append(wrapped_tool)
|
|
643
|
-
logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema suffix)" if tool_suffix else ""))
|
|
644
|
-
|
|
645
|
-
logger.info(f"Loaded {len(mcp_tools_dict)} tools from MCP server: {server_id} (in-process)")
|
|
646
|
-
|
|
647
|
-
except Exception as e:
|
|
648
|
-
logger.error(f"Failed to load MCP server {server_id}: {e}", exc_info=True)
|
|
649
|
-
else:
|
|
650
|
-
logger.warning(f"Unsupported MCP server type: {server_type}")
|
|
682
|
+
# Track loaded MCP servers for resource resolution
|
|
683
|
+
loaded_mcp_server = None
|
|
684
|
+
|
|
685
|
+
for server_config in mcp_server_configs:
|
|
686
|
+
server_type = server_config.get("type")
|
|
687
|
+
server_id = server_config.get("id", "mcp-server")
|
|
688
|
+
|
|
689
|
+
if server_type == "local":
|
|
690
|
+
# Import MCP server directly (in-process)
|
|
691
|
+
module_path = server_config.get("module", "rem.mcp_server")
|
|
692
|
+
|
|
693
|
+
try:
|
|
694
|
+
# Dynamic import of MCP server module
|
|
695
|
+
import importlib
|
|
696
|
+
mcp_module = importlib.import_module(module_path)
|
|
697
|
+
mcp_server = mcp_module.mcp
|
|
698
|
+
|
|
699
|
+
# Store the loaded server for resource resolution
|
|
700
|
+
loaded_mcp_server = mcp_server
|
|
701
|
+
|
|
702
|
+
# Extract tools from MCP server (get_tools is async)
|
|
703
|
+
from ..mcp.tool_wrapper import create_mcp_tool_wrapper
|
|
704
|
+
|
|
705
|
+
# Await async get_tools() call
|
|
706
|
+
mcp_tools_dict = await mcp_server.get_tools()
|
|
651
707
|
|
|
708
|
+
for tool_name, tool_func in mcp_tools_dict.items():
|
|
709
|
+
# Add description suffix to search_rem tool if schema specifies a default table
|
|
710
|
+
tool_suffix = search_rem_suffix if tool_name == "search_rem" else None
|
|
711
|
+
|
|
712
|
+
wrapped_tool = create_mcp_tool_wrapper(
|
|
713
|
+
tool_name,
|
|
714
|
+
tool_func,
|
|
715
|
+
user_id=context.user_id if context else None,
|
|
716
|
+
description_suffix=tool_suffix,
|
|
717
|
+
)
|
|
718
|
+
tools.append(wrapped_tool)
|
|
719
|
+
logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema suffix)" if tool_suffix else ""))
|
|
720
|
+
|
|
721
|
+
logger.info(f"Loaded {len(mcp_tools_dict)} tools from MCP server: {server_id} (in-process)")
|
|
722
|
+
|
|
723
|
+
except Exception as e:
|
|
724
|
+
logger.error(f"Failed to load MCP server {server_id}: {e}", exc_info=True)
|
|
725
|
+
else:
|
|
726
|
+
logger.warning(f"Unsupported MCP server type: {server_type}")
|
|
727
|
+
|
|
728
|
+
# Convert resources to tools (MCP convenience syntax)
|
|
729
|
+
# Resources declared in agent YAML become callable tools - eliminates
|
|
730
|
+
# the artificial MCP distinction between tools and resources
|
|
731
|
+
#
|
|
732
|
+
# Supports both concrete and template URIs:
|
|
733
|
+
# - Concrete: "rem://schemas" -> no-param tool
|
|
734
|
+
# - Template: "patient-profile://field/{field_key}" -> tool with field_key param
|
|
735
|
+
from ..mcp.tool_wrapper import create_resource_tool
|
|
736
|
+
|
|
737
|
+
# Collect all resource URIs from both resources section AND tools section
|
|
738
|
+
resource_uris = []
|
|
739
|
+
|
|
740
|
+
# From resources section (legacy format)
|
|
652
741
|
if resource_configs:
|
|
653
|
-
|
|
654
|
-
|
|
742
|
+
for resource_config in resource_configs:
|
|
743
|
+
if hasattr(resource_config, 'uri'):
|
|
744
|
+
uri = resource_config.uri
|
|
745
|
+
usage = resource_config.description or ""
|
|
746
|
+
else:
|
|
747
|
+
uri = resource_config.get("uri", "")
|
|
748
|
+
usage = resource_config.get("description", "")
|
|
749
|
+
if uri:
|
|
750
|
+
resource_uris.append((uri, usage))
|
|
751
|
+
|
|
752
|
+
# From tools section - detect URIs (anything with ://)
|
|
753
|
+
# This allows unified syntax: resources as tools
|
|
754
|
+
tool_configs = metadata.tools if metadata and hasattr(metadata, 'tools') else []
|
|
755
|
+
for tool_config in tool_configs:
|
|
756
|
+
if hasattr(tool_config, 'name'):
|
|
757
|
+
tool_name = tool_config.name
|
|
758
|
+
tool_desc = tool_config.description or ""
|
|
759
|
+
else:
|
|
760
|
+
tool_name = tool_config.get("name", "")
|
|
761
|
+
tool_desc = tool_config.get("description", "")
|
|
762
|
+
|
|
763
|
+
# Auto-detect resource URIs (anything with :// scheme)
|
|
764
|
+
if "://" in tool_name:
|
|
765
|
+
resource_uris.append((tool_name, tool_desc))
|
|
766
|
+
|
|
767
|
+
# Create tools from collected resource URIs
|
|
768
|
+
# Pass the loaded MCP server so resources can be resolved from it
|
|
769
|
+
for uri, usage in resource_uris:
|
|
770
|
+
resource_tool = create_resource_tool(uri, usage, mcp_server=loaded_mcp_server)
|
|
771
|
+
tools.append(resource_tool)
|
|
772
|
+
logger.debug(f"Loaded resource as tool: {uri}")
|
|
655
773
|
|
|
656
774
|
# Create dynamic result_type from schema if not provided
|
|
657
775
|
# Note: use_structured_output is set earlier from metadata.structured_output
|
rem/agentic/schema.py
CHANGED
|
@@ -154,8 +154,10 @@ class MCPServerConfig(BaseModel):
|
|
|
154
154
|
)
|
|
155
155
|
|
|
156
156
|
id: str = Field(
|
|
157
|
+
default="mcp-server",
|
|
157
158
|
description=(
|
|
158
159
|
"Server identifier for logging and debugging. "
|
|
160
|
+
"Defaults to 'mcp-server' if not specified. "
|
|
159
161
|
"Example: 'rem-local'"
|
|
160
162
|
)
|
|
161
163
|
)
|
|
@@ -213,12 +215,13 @@ class AgentSchemaMetadata(BaseModel):
|
|
|
213
215
|
)
|
|
214
216
|
|
|
215
217
|
# Structured output toggle
|
|
216
|
-
structured_output: bool = Field(
|
|
217
|
-
default=
|
|
218
|
+
structured_output: bool | None = Field(
|
|
219
|
+
default=None,
|
|
218
220
|
description=(
|
|
219
221
|
"Whether to enforce structured JSON output. "
|
|
220
222
|
"When False, the agent produces free-form text and schema properties "
|
|
221
|
-
"are converted to prompt guidance instead.
|
|
223
|
+
"are converted to prompt guidance instead. "
|
|
224
|
+
"Default: None (uses LLM__DEFAULT_STRUCTURED_OUTPUT setting, which defaults to False)."
|
|
222
225
|
),
|
|
223
226
|
)
|
|
224
227
|
|
|
@@ -228,7 +231,8 @@ class AgentSchemaMetadata(BaseModel):
|
|
|
228
231
|
description=(
|
|
229
232
|
"MCP server configurations for dynamic tool loading. "
|
|
230
233
|
"Servers are loaded in-process at agent creation time. "
|
|
231
|
-
"All tools from configured servers become available to the agent."
|
|
234
|
+
"All tools from configured servers become available to the agent. "
|
|
235
|
+
"If not specified, defaults to rem.mcp_server (REM's built-in tools)."
|
|
232
236
|
),
|
|
233
237
|
)
|
|
234
238
|
|
rem/api/deps.py
CHANGED
|
@@ -147,7 +147,6 @@ def is_admin(user: dict | None) -> bool:
|
|
|
147
147
|
async def get_user_filter(
|
|
148
148
|
request: Request,
|
|
149
149
|
x_user_id: str | None = None,
|
|
150
|
-
x_tenant_id: str = "default",
|
|
151
150
|
) -> dict[str, Any]:
|
|
152
151
|
"""
|
|
153
152
|
Get user-scoped filter dict for database queries.
|
|
@@ -158,7 +157,6 @@ async def get_user_filter(
|
|
|
158
157
|
Args:
|
|
159
158
|
request: FastAPI request
|
|
160
159
|
x_user_id: Optional user_id filter (admin only for cross-user)
|
|
161
|
-
x_tenant_id: Tenant ID for multi-tenancy
|
|
162
160
|
|
|
163
161
|
Returns:
|
|
164
162
|
Filter dict with appropriate user_id constraint
|
|
@@ -169,7 +167,7 @@ async def get_user_filter(
|
|
|
169
167
|
return await repo.find(filters)
|
|
170
168
|
"""
|
|
171
169
|
user = get_current_user(request)
|
|
172
|
-
filters: dict[str, Any] = {
|
|
170
|
+
filters: dict[str, Any] = {}
|
|
173
171
|
|
|
174
172
|
if is_admin(user):
|
|
175
173
|
# Admin can filter by any user or see all
|
|
@@ -185,8 +183,8 @@ async def get_user_filter(
|
|
|
185
183
|
f"User {user.get('email')} attempted to filter by user_id={x_user_id}"
|
|
186
184
|
)
|
|
187
185
|
else:
|
|
188
|
-
# Anonymous:
|
|
189
|
-
#
|
|
186
|
+
# Anonymous: use anonymous tracking ID
|
|
187
|
+
# Note: user_id should come from JWT, not from parameters
|
|
190
188
|
anon_id = getattr(request.state, "anon_id", None)
|
|
191
189
|
if anon_id:
|
|
192
190
|
filters["user_id"] = f"anon:{anon_id}"
|
rem/api/main.py
CHANGED
|
@@ -149,19 +149,38 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
149
149
|
client_host = request.client.host if request.client else "unknown"
|
|
150
150
|
user_agent = request.headers.get('user-agent', 'unknown')[:100]
|
|
151
151
|
|
|
152
|
+
# Extract auth info for logging (first 8 chars of token for debugging)
|
|
153
|
+
auth_header = request.headers.get('authorization', '')
|
|
154
|
+
auth_preview = ""
|
|
155
|
+
if auth_header.startswith('Bearer '):
|
|
156
|
+
token = auth_header[7:]
|
|
157
|
+
auth_preview = f"Bearer {token[:8]}..." if len(token) > 8 else f"Bearer {token}"
|
|
158
|
+
|
|
152
159
|
# Process request
|
|
153
160
|
response = await call_next(request)
|
|
154
161
|
|
|
162
|
+
# Extract user info set by auth middleware (after processing)
|
|
163
|
+
user = getattr(request.state, "user", None)
|
|
164
|
+
user_id = user.get("id", "none")[:12] if user else "anon"
|
|
165
|
+
user_email = user.get("email", "") if user else ""
|
|
166
|
+
|
|
155
167
|
# Determine log level based on path AND response status
|
|
156
168
|
duration_ms = (time.time() - start_time) * 1000
|
|
157
169
|
use_debug = self._should_log_at_debug(path, response.status_code)
|
|
158
170
|
log_fn = logger.debug if use_debug else logger.info
|
|
159
171
|
|
|
160
|
-
#
|
|
172
|
+
# Build user info string
|
|
173
|
+
user_info = f"user={user_id}"
|
|
174
|
+
if user_email:
|
|
175
|
+
user_info += f" ({user_email})"
|
|
176
|
+
if auth_preview:
|
|
177
|
+
user_info += f" | auth={auth_preview}"
|
|
178
|
+
|
|
179
|
+
# Log request and response together with auth info
|
|
161
180
|
log_fn(
|
|
162
181
|
f"→ REQUEST: {request.method} {path} | "
|
|
163
182
|
f"Client: {client_host} | "
|
|
164
|
-
f"
|
|
183
|
+
f"{user_info}"
|
|
165
184
|
)
|
|
166
185
|
log_fn(
|
|
167
186
|
f"← RESPONSE: {request.method} {path} | "
|
|
@@ -304,7 +323,7 @@ def create_app() -> FastAPI:
|
|
|
304
323
|
app.add_middleware(
|
|
305
324
|
AuthMiddleware,
|
|
306
325
|
protected_paths=["/api/v1"],
|
|
307
|
-
excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth"],
|
|
326
|
+
excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth", "/api/v1/slack"],
|
|
308
327
|
# Allow anonymous when auth is disabled, otherwise use setting
|
|
309
328
|
allow_anonymous=(not settings.auth.enabled) or settings.auth.allow_anonymous,
|
|
310
329
|
# MCP requires auth only when auth is fully enabled
|
rem/api/mcp_router/resources.py
CHANGED
|
@@ -512,9 +512,10 @@ async def load_resource(uri: str) -> dict | str:
|
|
|
512
512
|
Raises:
|
|
513
513
|
ValueError: If URI is invalid or resource not found
|
|
514
514
|
"""
|
|
515
|
-
|
|
515
|
+
import inspect
|
|
516
516
|
from fastmcp import FastMCP
|
|
517
517
|
|
|
518
|
+
# Create temporary MCP instance with resources
|
|
518
519
|
mcp = FastMCP(name="temp")
|
|
519
520
|
|
|
520
521
|
# Register all resources
|
|
@@ -523,14 +524,18 @@ async def load_resource(uri: str) -> dict | str:
|
|
|
523
524
|
register_file_resources(mcp)
|
|
524
525
|
register_status_resources(mcp)
|
|
525
526
|
|
|
526
|
-
# Get
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
527
|
+
# Get resources using FastMCP's async get_resources() method
|
|
528
|
+
resources = await mcp.get_resources()
|
|
529
|
+
|
|
530
|
+
if uri in resources:
|
|
531
|
+
resource = resources[uri]
|
|
532
|
+
# Call the underlying function
|
|
533
|
+
result = resource.fn()
|
|
534
|
+
# Handle async functions
|
|
535
|
+
if inspect.iscoroutine(result):
|
|
536
|
+
result = await result
|
|
537
|
+
return result if result else {"error": "Resource returned None"}
|
|
534
538
|
|
|
535
539
|
# If not found, raise error
|
|
536
|
-
|
|
540
|
+
available = list(resources.keys())
|
|
541
|
+
raise ValueError(f"Resource not found: {uri}. Available resources: {available}")
|
rem/api/mcp_router/server.py
CHANGED
|
@@ -182,6 +182,7 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
182
182
|
list_schema,
|
|
183
183
|
read_resource,
|
|
184
184
|
register_metadata,
|
|
185
|
+
save_agent,
|
|
185
186
|
search_rem,
|
|
186
187
|
)
|
|
187
188
|
|
|
@@ -191,6 +192,7 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
191
192
|
mcp.tool()(register_metadata)
|
|
192
193
|
mcp.tool()(list_schema)
|
|
193
194
|
mcp.tool()(get_schema)
|
|
195
|
+
mcp.tool()(save_agent)
|
|
194
196
|
|
|
195
197
|
# File ingestion tool (with local path support for local servers)
|
|
196
198
|
# Wrap to inject is_local parameter
|
rem/api/mcp_router/tools.py
CHANGED
|
@@ -116,7 +116,8 @@ def mcp_tool_error_handler(func: Callable) -> Callable:
|
|
|
116
116
|
# Otherwise wrap in success response
|
|
117
117
|
return {"status": "success", **result}
|
|
118
118
|
except Exception as e:
|
|
119
|
-
|
|
119
|
+
# Use %s format to avoid issues with curly braces in error messages
|
|
120
|
+
logger.opt(exception=True).error("{} failed: {}", func.__name__, str(e))
|
|
120
121
|
return {
|
|
121
122
|
"status": "error",
|
|
122
123
|
"error": str(e),
|
|
@@ -380,9 +381,10 @@ async def ask_rem_agent(
|
|
|
380
381
|
from ...utils.schema_loader import load_agent_schema
|
|
381
382
|
|
|
382
383
|
# Create agent context
|
|
384
|
+
# Note: tenant_id defaults to "default" if user_id is None
|
|
383
385
|
context = AgentContext(
|
|
384
386
|
user_id=user_id,
|
|
385
|
-
tenant_id=user_id, #
|
|
387
|
+
tenant_id=user_id or "default", # Use default tenant for anonymous users
|
|
386
388
|
default_model=settings.llm.default_model,
|
|
387
389
|
)
|
|
388
390
|
|
|
@@ -1040,3 +1042,93 @@ async def get_schema(
|
|
|
1040
1042
|
logger.info(f"Retrieved schema for table '{table_name}' with {len(column_defs)} columns")
|
|
1041
1043
|
|
|
1042
1044
|
return result
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
@mcp_tool_error_handler
|
|
1048
|
+
async def save_agent(
|
|
1049
|
+
name: str,
|
|
1050
|
+
description: str,
|
|
1051
|
+
properties: dict[str, Any] | None = None,
|
|
1052
|
+
required: list[str] | None = None,
|
|
1053
|
+
tools: list[str] | None = None,
|
|
1054
|
+
tags: list[str] | None = None,
|
|
1055
|
+
version: str = "1.0.0",
|
|
1056
|
+
user_id: str | None = None,
|
|
1057
|
+
) -> dict[str, Any]:
|
|
1058
|
+
"""
|
|
1059
|
+
Save an agent schema to REM, making it available for use.
|
|
1060
|
+
|
|
1061
|
+
This tool creates or updates an agent definition in the user's schema space.
|
|
1062
|
+
The agent becomes immediately available for conversations.
|
|
1063
|
+
|
|
1064
|
+
**Default Tools**: All agents automatically get `search_rem` and `register_metadata`
|
|
1065
|
+
tools unless explicitly overridden.
|
|
1066
|
+
|
|
1067
|
+
Args:
|
|
1068
|
+
name: Agent name in kebab-case (e.g., "code-reviewer", "sales-assistant").
|
|
1069
|
+
Must be unique within the user's schema space.
|
|
1070
|
+
description: The agent's system prompt. This is the full instruction set
|
|
1071
|
+
that defines the agent's behavior, personality, and capabilities.
|
|
1072
|
+
Use markdown formatting for structure.
|
|
1073
|
+
properties: Output schema properties as a dict. Each property should have:
|
|
1074
|
+
- type: "string", "number", "boolean", "array", "object"
|
|
1075
|
+
- description: What this field captures
|
|
1076
|
+
Example: {"answer": {"type": "string", "description": "Response to user"}}
|
|
1077
|
+
If not provided, defaults to a simple {"answer": {"type": "string"}} schema.
|
|
1078
|
+
required: List of required property names. Defaults to ["answer"] if not provided.
|
|
1079
|
+
tools: List of tool names the agent can use. Defaults to ["search_rem", "register_metadata"].
|
|
1080
|
+
tags: Optional tags for categorizing the agent.
|
|
1081
|
+
version: Semantic version string (default: "1.0.0").
|
|
1082
|
+
user_id: User identifier for scoping. Uses authenticated user if not provided.
|
|
1083
|
+
|
|
1084
|
+
Returns:
|
|
1085
|
+
Dict with:
|
|
1086
|
+
- status: "success" or "error"
|
|
1087
|
+
- agent_name: Name of the saved agent
|
|
1088
|
+
- version: Version saved
|
|
1089
|
+
- message: Human-readable status
|
|
1090
|
+
|
|
1091
|
+
Examples:
|
|
1092
|
+
# Create a simple agent
|
|
1093
|
+
save_agent(
|
|
1094
|
+
name="greeting-bot",
|
|
1095
|
+
description="You are a friendly greeter. Say hello warmly.",
|
|
1096
|
+
properties={"answer": {"type": "string", "description": "Greeting message"}},
|
|
1097
|
+
required=["answer"]
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# Create agent with structured output
|
|
1101
|
+
save_agent(
|
|
1102
|
+
name="sentiment-analyzer",
|
|
1103
|
+
description="Analyze sentiment of text provided by the user.",
|
|
1104
|
+
properties={
|
|
1105
|
+
"answer": {"type": "string", "description": "Analysis explanation"},
|
|
1106
|
+
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
|
|
1107
|
+
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
|
|
1108
|
+
},
|
|
1109
|
+
required=["answer", "sentiment"],
|
|
1110
|
+
tags=["analysis", "nlp"]
|
|
1111
|
+
)
|
|
1112
|
+
"""
|
|
1113
|
+
from ...agentic.agents.agent_manager import save_agent as _save_agent
|
|
1114
|
+
|
|
1115
|
+
# Get user_id from context if not provided
|
|
1116
|
+
user_id = AgentContext.get_user_id_or_default(user_id, source="save_agent")
|
|
1117
|
+
|
|
1118
|
+
# Delegate to agent_manager
|
|
1119
|
+
result = await _save_agent(
|
|
1120
|
+
name=name,
|
|
1121
|
+
description=description,
|
|
1122
|
+
user_id=user_id,
|
|
1123
|
+
properties=properties,
|
|
1124
|
+
required=required,
|
|
1125
|
+
tools=tools,
|
|
1126
|
+
tags=tags,
|
|
1127
|
+
version=version,
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
# Add helpful message for Slack users
|
|
1131
|
+
if result.get("status") == "success":
|
|
1132
|
+
result["message"] = f"Agent '{name}' saved. Use `/custom-agent {name}` to chat with it."
|
|
1133
|
+
|
|
1134
|
+
return result
|
rem/api/middleware/tracking.py
CHANGED
|
@@ -102,14 +102,14 @@ class AnonymousTrackingMiddleware(BaseHTTPMiddleware):
|
|
|
102
102
|
# Tenant ID from header or default
|
|
103
103
|
tenant_id = request.headers.get("X-Tenant-Id", "default")
|
|
104
104
|
|
|
105
|
-
# 4. Rate Limiting
|
|
106
|
-
if settings.postgres.enabled:
|
|
105
|
+
# 4. Rate Limiting (skip if disabled via settings)
|
|
106
|
+
if settings.postgres.enabled and settings.api.rate_limit_enabled:
|
|
107
107
|
is_allowed, current, limit = await self.rate_limiter.check_rate_limit(
|
|
108
108
|
tenant_id=tenant_id,
|
|
109
109
|
identifier=identifier,
|
|
110
110
|
tier=tier
|
|
111
111
|
)
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
if not is_allowed:
|
|
114
114
|
return JSONResponse(
|
|
115
115
|
status_code=429,
|
|
@@ -141,8 +141,8 @@ class AnonymousTrackingMiddleware(BaseHTTPMiddleware):
|
|
|
141
141
|
secure=settings.environment == "production"
|
|
142
142
|
)
|
|
143
143
|
|
|
144
|
-
# Add Rate Limit headers
|
|
145
|
-
if settings.postgres.enabled and 'limit' in locals():
|
|
144
|
+
# Add Rate Limit headers (only if rate limiting is enabled)
|
|
145
|
+
if settings.postgres.enabled and settings.api.rate_limit_enabled and 'limit' in locals():
|
|
146
146
|
response.headers["X-RateLimit-Limit"] = str(limit)
|
|
147
147
|
response.headers["X-RateLimit-Remaining"] = str(max(0, limit - current))
|
|
148
148
|
|