remdb 0.3.133__py3-none-any.whl → 0.3.171__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.
- 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 +54 -6
- rem/agentic/providers/phoenix.py +91 -21
- rem/agentic/providers/pydantic_ai.py +88 -45
- rem/api/deps.py +3 -5
- rem/api/main.py +22 -3
- 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 +50 -49
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +2 -0
- rem/models/core/experiment.py +4 -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 +200 -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.133.dist-info → remdb-0.3.171.dist-info}/METADATA +7 -5
- {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/RECORD +60 -50
- {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/WHEEL +0 -0
- {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/entry_points.txt +0 -0
rem/agentic/providers/phoenix.py
CHANGED
|
@@ -94,6 +94,82 @@ def _check_phoenix_available() -> bool:
|
|
|
94
94
|
return PHOENIX_AVAILABLE
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def validate_evaluator_credentials(
|
|
98
|
+
model_name: str | None = None,
|
|
99
|
+
) -> tuple[bool, str | None]:
|
|
100
|
+
"""Validate that the evaluator's LLM provider has working credentials.
|
|
101
|
+
|
|
102
|
+
Performs a minimal API call to verify credentials before running experiments.
|
|
103
|
+
This prevents running expensive agent tasks only to have evaluations fail.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
model_name: Model to validate (defaults to claude-sonnet-4-5-20250929)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Tuple of (success: bool, error_message: str | None)
|
|
110
|
+
- (True, None) if credentials are valid
|
|
111
|
+
- (False, "error description") if validation fails
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> success, error = validate_evaluator_credentials()
|
|
115
|
+
>>> if not success:
|
|
116
|
+
... print(f"Evaluator validation failed: {error}")
|
|
117
|
+
... return
|
|
118
|
+
"""
|
|
119
|
+
if not _check_phoenix_available():
|
|
120
|
+
return False, "arize-phoenix package not installed"
|
|
121
|
+
|
|
122
|
+
from phoenix.evals import OpenAIModel, AnthropicModel
|
|
123
|
+
|
|
124
|
+
# Default model (check env var first)
|
|
125
|
+
if model_name is None:
|
|
126
|
+
import os
|
|
127
|
+
model_name = os.environ.get("EVALUATOR_MODEL", "claude-sonnet-4-5-20250929")
|
|
128
|
+
|
|
129
|
+
# Parse provider
|
|
130
|
+
if ":" in model_name:
|
|
131
|
+
provider, phoenix_model_name = model_name.split(":", 1)
|
|
132
|
+
else:
|
|
133
|
+
if model_name.startswith("claude"):
|
|
134
|
+
provider = "anthropic"
|
|
135
|
+
else:
|
|
136
|
+
provider = "openai"
|
|
137
|
+
phoenix_model_name = model_name
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Create LLM wrapper
|
|
141
|
+
if provider.lower() == "anthropic":
|
|
142
|
+
llm = AnthropicModel(
|
|
143
|
+
model=phoenix_model_name,
|
|
144
|
+
temperature=0.0,
|
|
145
|
+
top_p=None,
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
llm = OpenAIModel(model=phoenix_model_name, temperature=0.0)
|
|
149
|
+
|
|
150
|
+
# Test with minimal prompt
|
|
151
|
+
logger.info(f"Validating evaluator credentials for {provider}:{phoenix_model_name}")
|
|
152
|
+
response = llm("Say 'ok' if you can read this.")
|
|
153
|
+
|
|
154
|
+
if response and len(response) > 0:
|
|
155
|
+
logger.info(f"Evaluator credentials validated successfully for {provider}")
|
|
156
|
+
return True, None
|
|
157
|
+
else:
|
|
158
|
+
return False, f"Empty response from {provider} model"
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
error_msg = str(e)
|
|
162
|
+
# Extract meaningful error from common API errors
|
|
163
|
+
if "credit balance is too low" in error_msg.lower():
|
|
164
|
+
return False, f"Anthropic API credits exhausted. Add credits at https://console.anthropic.com/settings/billing"
|
|
165
|
+
elif "api key" in error_msg.lower() or "authentication" in error_msg.lower():
|
|
166
|
+
return False, f"{provider.capitalize()} API key missing or invalid. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable."
|
|
167
|
+
elif "rate limit" in error_msg.lower():
|
|
168
|
+
return False, f"{provider.capitalize()} rate limit exceeded. Wait and retry."
|
|
169
|
+
else:
|
|
170
|
+
return False, f"{provider.capitalize()} API error: {error_msg[:200]}"
|
|
171
|
+
|
|
172
|
+
|
|
97
173
|
# =============================================================================
|
|
98
174
|
# NAME SANITIZATION
|
|
99
175
|
# =============================================================================
|
|
@@ -207,8 +283,9 @@ def create_phoenix_evaluator(
|
|
|
207
283
|
|
|
208
284
|
# Default model (use Claude Sonnet 4.5 for evaluators)
|
|
209
285
|
if model_name is None:
|
|
210
|
-
|
|
211
|
-
|
|
286
|
+
import os
|
|
287
|
+
model_name = os.environ.get("EVALUATOR_MODEL", "claude-sonnet-4-5-20250929")
|
|
288
|
+
logger.debug(f"Using evaluator model: {model_name}")
|
|
212
289
|
|
|
213
290
|
logger.info(f"Creating Phoenix evaluator: {evaluator_name} with model={model_name}")
|
|
214
291
|
|
|
@@ -589,33 +666,26 @@ Please evaluate the agent's answer according to the evaluation criteria."""
|
|
|
589
666
|
|
|
590
667
|
logger.debug(f"Created {len(evaluations)} evaluations")
|
|
591
668
|
|
|
592
|
-
# Phoenix
|
|
593
|
-
#
|
|
594
|
-
from phoenix.experiments.evaluators.base import EvaluationResult
|
|
595
|
-
|
|
669
|
+
# Phoenix client expects a dict with score, label, explanation
|
|
670
|
+
# (not the old EvaluationResult class)
|
|
596
671
|
overall_eval = next(
|
|
597
672
|
(e for e in evaluations if e["name"] == "overall"),
|
|
598
673
|
{"score": 0.0, "label": "unknown", "explanation": None}
|
|
599
674
|
)
|
|
600
675
|
|
|
601
|
-
return
|
|
602
|
-
score
|
|
603
|
-
label
|
|
604
|
-
explanation
|
|
605
|
-
|
|
606
|
-
"evaluations": evaluations,
|
|
607
|
-
"raw_response": response_json,
|
|
608
|
-
}
|
|
609
|
-
)
|
|
676
|
+
return {
|
|
677
|
+
"score": overall_eval.get("score", 0.0),
|
|
678
|
+
"label": overall_eval.get("label", "unknown"),
|
|
679
|
+
"explanation": overall_eval.get("explanation"),
|
|
680
|
+
}
|
|
610
681
|
|
|
611
682
|
except Exception as e:
|
|
612
683
|
logger.error(f"Evaluator error: {e}")
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
)
|
|
684
|
+
return {
|
|
685
|
+
"score": 0.0,
|
|
686
|
+
"label": "error",
|
|
687
|
+
"explanation": f"Evaluator failed: {str(e)}",
|
|
688
|
+
}
|
|
619
689
|
|
|
620
690
|
return evaluator_fn
|
|
621
691
|
|
|
@@ -550,10 +550,18 @@ async def create_agent(
|
|
|
550
550
|
# Extract schema fields using typed helpers
|
|
551
551
|
from ..schema import get_system_prompt, get_metadata
|
|
552
552
|
|
|
553
|
+
# Track whether mcp_servers was explicitly configured (even if empty)
|
|
554
|
+
mcp_servers_explicitly_set = False
|
|
555
|
+
|
|
553
556
|
if agent_schema:
|
|
554
557
|
system_prompt = get_system_prompt(agent_schema)
|
|
555
558
|
metadata = get_metadata(agent_schema)
|
|
556
|
-
|
|
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 = []
|
|
557
565
|
resource_configs = metadata.resources if hasattr(metadata, 'resources') else []
|
|
558
566
|
|
|
559
567
|
if metadata.system_prompt:
|
|
@@ -564,9 +572,38 @@ async def create_agent(
|
|
|
564
572
|
mcp_server_configs = []
|
|
565
573
|
resource_configs = []
|
|
566
574
|
|
|
567
|
-
#
|
|
568
|
-
|
|
569
|
-
|
|
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"}]
|
|
570
607
|
|
|
571
608
|
# Extract temperature and max_iterations from schema metadata (with fallback to settings defaults)
|
|
572
609
|
if metadata:
|
|
@@ -612,46 +649,51 @@ async def create_agent(
|
|
|
612
649
|
search_rem_suffix += f"Example: `SEARCH \"your query\" FROM {default_table} LIMIT 10`"
|
|
613
650
|
|
|
614
651
|
# Add tools from MCP server (in-process, no subprocess)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
logger.
|
|
653
|
-
|
|
654
|
-
logger.
|
|
652
|
+
# Track loaded MCP servers for resource resolution
|
|
653
|
+
loaded_mcp_server = None
|
|
654
|
+
|
|
655
|
+
for server_config in mcp_server_configs:
|
|
656
|
+
server_type = server_config.get("type")
|
|
657
|
+
server_id = server_config.get("id", "mcp-server")
|
|
658
|
+
|
|
659
|
+
if server_type == "local":
|
|
660
|
+
# Import MCP server directly (in-process)
|
|
661
|
+
module_path = server_config.get("module", "rem.mcp_server")
|
|
662
|
+
|
|
663
|
+
try:
|
|
664
|
+
# Dynamic import of MCP server module
|
|
665
|
+
import importlib
|
|
666
|
+
mcp_module = importlib.import_module(module_path)
|
|
667
|
+
mcp_server = mcp_module.mcp
|
|
668
|
+
|
|
669
|
+
# Store the loaded server for resource resolution
|
|
670
|
+
loaded_mcp_server = mcp_server
|
|
671
|
+
|
|
672
|
+
# Extract tools from MCP server (get_tools is async)
|
|
673
|
+
from ..mcp.tool_wrapper import create_mcp_tool_wrapper
|
|
674
|
+
|
|
675
|
+
# Await async get_tools() call
|
|
676
|
+
mcp_tools_dict = await mcp_server.get_tools()
|
|
677
|
+
|
|
678
|
+
for tool_name, tool_func in mcp_tools_dict.items():
|
|
679
|
+
# Add description suffix to search_rem tool if schema specifies a default table
|
|
680
|
+
tool_suffix = search_rem_suffix if tool_name == "search_rem" else None
|
|
681
|
+
|
|
682
|
+
wrapped_tool = create_mcp_tool_wrapper(
|
|
683
|
+
tool_name,
|
|
684
|
+
tool_func,
|
|
685
|
+
user_id=context.user_id if context else None,
|
|
686
|
+
description_suffix=tool_suffix,
|
|
687
|
+
)
|
|
688
|
+
tools.append(wrapped_tool)
|
|
689
|
+
logger.debug(f"Loaded MCP tool: {tool_name}" + (" (with schema suffix)" if tool_suffix else ""))
|
|
690
|
+
|
|
691
|
+
logger.info(f"Loaded {len(mcp_tools_dict)} tools from MCP server: {server_id} (in-process)")
|
|
692
|
+
|
|
693
|
+
except Exception as e:
|
|
694
|
+
logger.error(f"Failed to load MCP server {server_id}: {e}", exc_info=True)
|
|
695
|
+
else:
|
|
696
|
+
logger.warning(f"Unsupported MCP server type: {server_type}")
|
|
655
697
|
|
|
656
698
|
# Convert resources to tools (MCP convenience syntax)
|
|
657
699
|
# Resources declared in agent YAML become callable tools - eliminates
|
|
@@ -693,8 +735,9 @@ async def create_agent(
|
|
|
693
735
|
resource_uris.append((tool_name, tool_desc))
|
|
694
736
|
|
|
695
737
|
# Create tools from collected resource URIs
|
|
738
|
+
# Pass the loaded MCP server so resources can be resolved from it
|
|
696
739
|
for uri, usage in resource_uris:
|
|
697
|
-
resource_tool = create_resource_tool(uri, usage)
|
|
740
|
+
resource_tool = create_resource_tool(uri, usage, mcp_server=loaded_mcp_server)
|
|
698
741
|
tools.append(resource_tool)
|
|
699
742
|
logger.debug(f"Loaded resource as tool: {uri}")
|
|
700
743
|
|
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/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
|
|