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/auth/middleware.py
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Authentication Middleware for FastAPI.
|
|
3
3
|
|
|
4
|
-
Protects API endpoints by requiring valid
|
|
5
|
-
Supports
|
|
4
|
+
Protects API endpoints by requiring valid authentication.
|
|
5
|
+
Supports multiple auth methods: JWT, API Key, Session, Dev Token.
|
|
6
|
+
Anonymous access with rate limiting when allow_anonymous=True.
|
|
6
7
|
MCP endpoints are always protected unless explicitly disabled.
|
|
7
8
|
|
|
8
9
|
Design Pattern:
|
|
9
|
-
-
|
|
10
|
-
-
|
|
10
|
+
- API Key (X-API-Key): Access control guardrail, NOT user identity
|
|
11
|
+
- JWT (Authorization: Bearer): Primary method for user identity
|
|
12
|
+
- Dev token: Non-production testing (starts with "dev_")
|
|
13
|
+
- Session: Backward compatibility for browser-based auth
|
|
11
14
|
- MCP paths always require authentication (protected service)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
Authentication Flow:
|
|
17
|
+
1. If API key enabled: Validate X-API-Key header (access gate)
|
|
18
|
+
2. Check JWT token for user identity (primary)
|
|
19
|
+
3. Check dev token for testing (non-production only)
|
|
20
|
+
4. Check session for user (backward compatibility)
|
|
21
|
+
5. If allow_anonymous=True: Allow as anonymous (rate-limited)
|
|
22
|
+
6. If allow_anonymous=False: Return 401 / redirect to login
|
|
23
|
+
|
|
24
|
+
IMPORTANT: API key validates ACCESS, JWT identifies USER.
|
|
25
|
+
Both can be required: API key for access + JWT for user identity.
|
|
15
26
|
|
|
16
27
|
Access Modes (configured in settings.auth):
|
|
17
28
|
- enabled=true, allow_anonymous=true: Auth available, anonymous gets rate-limited access
|
|
@@ -20,6 +31,11 @@ Access Modes (configured in settings.auth):
|
|
|
20
31
|
- mcp_requires_auth=true (default): MCP always requires login regardless of allow_anonymous
|
|
21
32
|
- mcp_requires_auth=false: MCP follows normal allow_anonymous rules (dev only)
|
|
22
33
|
|
|
34
|
+
API Key Authentication (configured in settings.api):
|
|
35
|
+
- api_key_enabled=true: Require X-API-Key header for access
|
|
36
|
+
- api_key: The secret key to validate against
|
|
37
|
+
- API key is an ACCESS GATE, not user identity - JWT still needed for user
|
|
38
|
+
|
|
23
39
|
Dev Token Support (non-production only):
|
|
24
40
|
- GET /api/auth/dev/token returns a Bearer token for test-user
|
|
25
41
|
- Include as: Authorization: Bearer dev_<signature>
|
|
@@ -82,6 +98,67 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
82
98
|
self.mcp_requires_auth = mcp_requires_auth
|
|
83
99
|
self.mcp_path = mcp_path
|
|
84
100
|
|
|
101
|
+
def _check_api_key(self, request: Request) -> dict | None:
|
|
102
|
+
"""
|
|
103
|
+
Check for valid X-API-Key header.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
API key user dict if valid, None otherwise
|
|
107
|
+
"""
|
|
108
|
+
# Only check if API key auth is enabled
|
|
109
|
+
if not settings.api.api_key_enabled:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
# Check for X-API-Key header
|
|
113
|
+
api_key = request.headers.get("x-api-key")
|
|
114
|
+
if not api_key:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
# Validate against configured API key
|
|
118
|
+
if settings.api.api_key and api_key == settings.api.api_key:
|
|
119
|
+
logger.debug("X-API-Key authenticated")
|
|
120
|
+
return {
|
|
121
|
+
"id": "api-key-user",
|
|
122
|
+
"email": "api@rem.local",
|
|
123
|
+
"name": "API Key User",
|
|
124
|
+
"provider": "api-key",
|
|
125
|
+
"tenant_id": "default",
|
|
126
|
+
"tier": "pro", # API key users get full access
|
|
127
|
+
"roles": ["user"],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Invalid API key
|
|
131
|
+
logger.warning("Invalid X-API-Key provided")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def _check_jwt_token(self, request: Request) -> dict | None:
|
|
135
|
+
"""
|
|
136
|
+
Check for valid JWT in Authorization header.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
User dict if valid JWT, None otherwise
|
|
140
|
+
"""
|
|
141
|
+
auth_header = request.headers.get("authorization", "")
|
|
142
|
+
if not auth_header.startswith("Bearer "):
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
token = auth_header[7:] # Strip "Bearer "
|
|
146
|
+
|
|
147
|
+
# Skip dev tokens (handled separately)
|
|
148
|
+
if token.startswith("dev_"):
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# Verify JWT token
|
|
152
|
+
from .jwt import get_jwt_service
|
|
153
|
+
jwt_service = get_jwt_service()
|
|
154
|
+
user = jwt_service.verify_token(token)
|
|
155
|
+
|
|
156
|
+
if user:
|
|
157
|
+
logger.debug(f"JWT authenticated: {user.get('email')}")
|
|
158
|
+
return user
|
|
159
|
+
|
|
160
|
+
return None
|
|
161
|
+
|
|
85
162
|
def _check_dev_token(self, request: Request) -> dict | None:
|
|
86
163
|
"""
|
|
87
164
|
Check for valid dev token in Authorization header (non-production only).
|
|
@@ -105,7 +182,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
105
182
|
# Verify dev token
|
|
106
183
|
from ..api.routers.dev import verify_dev_token
|
|
107
184
|
if verify_dev_token(token):
|
|
108
|
-
logger.debug(
|
|
185
|
+
logger.debug("Dev token authenticated as test-user")
|
|
109
186
|
return {
|
|
110
187
|
"id": "test-user",
|
|
111
188
|
"email": "test@rem.local",
|
|
@@ -142,6 +219,34 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
142
219
|
if not is_protected or is_excluded:
|
|
143
220
|
return await call_next(request)
|
|
144
221
|
|
|
222
|
+
# API key validation (access control, not user identity)
|
|
223
|
+
# API key is a guardrail for access - JWT identifies the actual user
|
|
224
|
+
if settings.api.api_key_enabled:
|
|
225
|
+
api_key = request.headers.get("x-api-key")
|
|
226
|
+
if not api_key:
|
|
227
|
+
logger.debug(f"Missing X-API-Key for: {path}")
|
|
228
|
+
return JSONResponse(
|
|
229
|
+
status_code=401,
|
|
230
|
+
content={"detail": "API key required. Include X-API-Key header."},
|
|
231
|
+
headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
|
|
232
|
+
)
|
|
233
|
+
if api_key != settings.api.api_key:
|
|
234
|
+
logger.warning(f"Invalid X-API-Key for: {path}")
|
|
235
|
+
return JSONResponse(
|
|
236
|
+
status_code=401,
|
|
237
|
+
content={"detail": "Invalid API key"},
|
|
238
|
+
headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
|
|
239
|
+
)
|
|
240
|
+
logger.debug("X-API-Key validated for access")
|
|
241
|
+
# API key valid - continue to check JWT for user identity
|
|
242
|
+
|
|
243
|
+
# Check for JWT token in Authorization header (primary user identity)
|
|
244
|
+
jwt_user = self._check_jwt_token(request)
|
|
245
|
+
if jwt_user:
|
|
246
|
+
request.state.user = jwt_user
|
|
247
|
+
request.state.is_anonymous = False
|
|
248
|
+
return await call_next(request)
|
|
249
|
+
|
|
145
250
|
# Check for dev token (non-production only)
|
|
146
251
|
dev_user = self._check_dev_token(request)
|
|
147
252
|
if dev_user:
|
|
@@ -149,7 +254,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
149
254
|
request.state.is_anonymous = False
|
|
150
255
|
return await call_next(request)
|
|
151
256
|
|
|
152
|
-
# Check for valid session
|
|
257
|
+
# Check for valid session (backward compatibility)
|
|
153
258
|
user = request.session.get("user")
|
|
154
259
|
|
|
155
260
|
if user:
|
rem/auth/providers/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Authentication provider implementations."""
|
|
2
2
|
|
|
3
3
|
from .base import OAuthProvider, OAuthTokens, OAuthUserInfo
|
|
4
|
+
from .email import EmailAuthProvider, EmailAuthResult
|
|
4
5
|
from .google import GoogleOAuthProvider
|
|
5
6
|
from .microsoft import MicrosoftOAuthProvider
|
|
6
7
|
|
|
@@ -8,6 +9,8 @@ __all__ = [
|
|
|
8
9
|
"OAuthProvider",
|
|
9
10
|
"OAuthTokens",
|
|
10
11
|
"OAuthUserInfo",
|
|
12
|
+
"EmailAuthProvider",
|
|
13
|
+
"EmailAuthResult",
|
|
11
14
|
"GoogleOAuthProvider",
|
|
12
15
|
"MicrosoftOAuthProvider",
|
|
13
16
|
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Email Authentication Provider.
|
|
3
|
+
|
|
4
|
+
Passwordless authentication using email verification codes.
|
|
5
|
+
Unlike OAuth providers, this handles the full flow internally.
|
|
6
|
+
|
|
7
|
+
Flow:
|
|
8
|
+
1. User requests login with email address
|
|
9
|
+
2. System generates code, upserts user, sends email
|
|
10
|
+
3. User enters code
|
|
11
|
+
4. System verifies code and creates session
|
|
12
|
+
|
|
13
|
+
Design:
|
|
14
|
+
- Uses EmailService for sending codes
|
|
15
|
+
- Creates users with deterministic UUID from email hash
|
|
16
|
+
- Stores challenge in user metadata
|
|
17
|
+
- No external OAuth dependencies
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
from loguru import logger
|
|
23
|
+
|
|
24
|
+
from ...services.email import EmailService
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from ...services.postgres import PostgresService
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EmailAuthResult(BaseModel):
|
|
31
|
+
"""Result of email authentication operations."""
|
|
32
|
+
|
|
33
|
+
success: bool = Field(description="Whether operation succeeded")
|
|
34
|
+
email: str = Field(description="Email address")
|
|
35
|
+
user_id: str | None = Field(default=None, description="User ID if authenticated")
|
|
36
|
+
error: str | None = Field(default=None, description="Error message if failed")
|
|
37
|
+
message: str | None = Field(default=None, description="User-friendly message")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EmailAuthProvider:
|
|
41
|
+
"""
|
|
42
|
+
Email-based passwordless authentication provider.
|
|
43
|
+
|
|
44
|
+
Handles the complete email login flow:
|
|
45
|
+
1. send_code() - Generate and send verification code
|
|
46
|
+
2. verify_code() - Verify code and return user info
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
email_service: EmailService | None = None,
|
|
52
|
+
template_kwargs: dict | None = None,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize EmailAuthProvider.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
email_service: EmailService instance (creates new one if not provided)
|
|
59
|
+
template_kwargs: Customization for email templates (colors, branding, etc.)
|
|
60
|
+
"""
|
|
61
|
+
self._email_service = email_service or EmailService()
|
|
62
|
+
self._template_kwargs = template_kwargs or {}
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_configured(self) -> bool:
|
|
66
|
+
"""Check if email auth is properly configured."""
|
|
67
|
+
return self._email_service.is_configured
|
|
68
|
+
|
|
69
|
+
async def send_code(
|
|
70
|
+
self,
|
|
71
|
+
email: str,
|
|
72
|
+
db: "PostgresService",
|
|
73
|
+
tenant_id: str = "default",
|
|
74
|
+
) -> EmailAuthResult:
|
|
75
|
+
"""
|
|
76
|
+
Send a verification code to an email address.
|
|
77
|
+
|
|
78
|
+
Creates user if not exists (using deterministic UUID from email).
|
|
79
|
+
Stores code in user metadata.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
email: Email address to send code to
|
|
83
|
+
db: PostgresService instance
|
|
84
|
+
tenant_id: Tenant identifier
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
EmailAuthResult with success status
|
|
88
|
+
"""
|
|
89
|
+
if not self.is_configured:
|
|
90
|
+
return EmailAuthResult(
|
|
91
|
+
success=False,
|
|
92
|
+
email=email,
|
|
93
|
+
error="Email service not configured",
|
|
94
|
+
message="Email login is not available. Please try another method.",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
result = await self._email_service.send_login_code(
|
|
99
|
+
email=email,
|
|
100
|
+
db=db,
|
|
101
|
+
tenant_id=tenant_id,
|
|
102
|
+
template_kwargs=self._template_kwargs,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if result["success"]:
|
|
106
|
+
return EmailAuthResult(
|
|
107
|
+
success=True,
|
|
108
|
+
email=email,
|
|
109
|
+
user_id=result["user_id"],
|
|
110
|
+
message=f"Verification code sent to {email}. Check your inbox.",
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
return EmailAuthResult(
|
|
114
|
+
success=False,
|
|
115
|
+
email=email,
|
|
116
|
+
error=result.get("error", "Failed to send code"),
|
|
117
|
+
message="Failed to send verification code. Please try again.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error sending login code: {e}")
|
|
122
|
+
return EmailAuthResult(
|
|
123
|
+
success=False,
|
|
124
|
+
email=email,
|
|
125
|
+
error=str(e),
|
|
126
|
+
message="An error occurred. Please try again.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def verify_code(
|
|
130
|
+
self,
|
|
131
|
+
email: str,
|
|
132
|
+
code: str,
|
|
133
|
+
db: "PostgresService",
|
|
134
|
+
tenant_id: str = "default",
|
|
135
|
+
) -> EmailAuthResult:
|
|
136
|
+
"""
|
|
137
|
+
Verify a login code and authenticate user.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
email: Email address
|
|
141
|
+
code: 6-digit verification code
|
|
142
|
+
db: PostgresService instance
|
|
143
|
+
tenant_id: Tenant identifier
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
EmailAuthResult with user_id if successful
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
result = await self._email_service.verify_login_code(
|
|
150
|
+
email=email,
|
|
151
|
+
code=code,
|
|
152
|
+
db=db,
|
|
153
|
+
tenant_id=tenant_id,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if result["valid"]:
|
|
157
|
+
return EmailAuthResult(
|
|
158
|
+
success=True,
|
|
159
|
+
email=email,
|
|
160
|
+
user_id=result["user_id"],
|
|
161
|
+
message="Successfully authenticated!",
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
error = result.get("error", "Invalid code")
|
|
165
|
+
# User-friendly error messages
|
|
166
|
+
if error == "Login code expired":
|
|
167
|
+
message = "Your code has expired. Please request a new one."
|
|
168
|
+
elif error == "Invalid login code":
|
|
169
|
+
message = "Invalid code. Please check and try again."
|
|
170
|
+
elif error == "No login code requested":
|
|
171
|
+
message = "No code was requested for this email. Please request a new code."
|
|
172
|
+
elif error == "User not found":
|
|
173
|
+
message = "Email not found. Please request a login code first."
|
|
174
|
+
else:
|
|
175
|
+
message = "Verification failed. Please try again."
|
|
176
|
+
|
|
177
|
+
return EmailAuthResult(
|
|
178
|
+
success=False,
|
|
179
|
+
email=email,
|
|
180
|
+
error=error,
|
|
181
|
+
message=message,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Error verifying login code: {e}")
|
|
186
|
+
return EmailAuthResult(
|
|
187
|
+
success=False,
|
|
188
|
+
email=email,
|
|
189
|
+
error=str(e),
|
|
190
|
+
message="An error occurred. Please try again.",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def get_user_dict(self, email: str, user_id: str) -> dict:
|
|
194
|
+
"""
|
|
195
|
+
Create a user dict for session storage.
|
|
196
|
+
|
|
197
|
+
Compatible with OAuth user format for consistent session handling.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
email: User's email
|
|
201
|
+
user_id: User's UUID
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
User dict for session
|
|
205
|
+
"""
|
|
206
|
+
return {
|
|
207
|
+
"id": user_id,
|
|
208
|
+
"email": email,
|
|
209
|
+
"email_verified": True, # Email is verified through code
|
|
210
|
+
"name": email.split("@")[0], # Use email prefix as name
|
|
211
|
+
"provider": "email",
|
|
212
|
+
"tenant_id": "default",
|
|
213
|
+
"tier": "free", # Email users start at free tier
|
|
214
|
+
"roles": ["user"],
|
|
215
|
+
}
|
rem/cli/commands/configure.py
CHANGED
|
@@ -110,7 +110,7 @@ def prompt_llm_config(use_defaults: bool = False) -> dict:
|
|
|
110
110
|
config = {}
|
|
111
111
|
|
|
112
112
|
# Default values
|
|
113
|
-
default_model = "
|
|
113
|
+
default_model = "openai:gpt-4.1"
|
|
114
114
|
default_temperature = 0.5
|
|
115
115
|
|
|
116
116
|
if use_defaults:
|
|
@@ -124,9 +124,9 @@ def prompt_llm_config(use_defaults: bool = False) -> dict:
|
|
|
124
124
|
# Default model
|
|
125
125
|
click.echo("\nDefault LLM model (format: provider:model-id)")
|
|
126
126
|
click.echo("Examples:")
|
|
127
|
+
click.echo(" - openai:gpt-4.1")
|
|
127
128
|
click.echo(" - anthropic:claude-sonnet-4-5-20250929")
|
|
128
|
-
click.echo(" - openai:gpt-
|
|
129
|
-
click.echo(" - openai:gpt-4o-mini")
|
|
129
|
+
click.echo(" - openai:gpt-4.1-mini")
|
|
130
130
|
|
|
131
131
|
config["default_model"] = click.prompt(
|
|
132
132
|
"Default model", default=default_model
|
|
@@ -422,7 +422,6 @@ def configure_command(install: bool, claude_desktop: bool, show: bool, edit: boo
|
|
|
422
422
|
|
|
423
423
|
try:
|
|
424
424
|
import shutil
|
|
425
|
-
from pathlib import Path
|
|
426
425
|
from fastmcp.mcp_config import update_config_file, StdioMCPServer
|
|
427
426
|
|
|
428
427
|
# Find Claude Desktop config path
|
rem/cli/commands/experiments.py
CHANGED
|
@@ -125,19 +125,17 @@ def create(
|
|
|
125
125
|
# Resolve base path: CLI arg > EXPERIMENTS_HOME env var > default "experiments"
|
|
126
126
|
if base_path is None:
|
|
127
127
|
base_path = os.getenv("EXPERIMENTS_HOME", "experiments")
|
|
128
|
-
# Build dataset reference
|
|
128
|
+
# Build dataset reference (format auto-detected from file extension)
|
|
129
129
|
if dataset_location == "git":
|
|
130
130
|
dataset_ref = DatasetReference(
|
|
131
131
|
location=DatasetLocation.GIT,
|
|
132
132
|
path="ground-truth/dataset.csv",
|
|
133
|
-
format="csv",
|
|
134
133
|
description="Ground truth Q&A dataset for evaluation"
|
|
135
134
|
)
|
|
136
135
|
else: # s3 or hybrid
|
|
137
136
|
dataset_ref = DatasetReference(
|
|
138
137
|
location=DatasetLocation(dataset_location),
|
|
139
138
|
path=f"s3://rem-experiments/{name}/datasets/ground_truth.parquet",
|
|
140
|
-
format="parquet",
|
|
141
139
|
schema_path="datasets/schema.yaml" if dataset_location == "hybrid" else None,
|
|
142
140
|
description="Ground truth dataset for evaluation"
|
|
143
141
|
)
|
|
@@ -915,58 +913,61 @@ def run(
|
|
|
915
913
|
click.echo(f" Last error: {evaluator_load_error}")
|
|
916
914
|
raise click.Abort()
|
|
917
915
|
|
|
918
|
-
#
|
|
919
|
-
|
|
916
|
+
# Validate evaluator credentials before running expensive agent tasks
|
|
917
|
+
if evaluator_fn is not None and not only_vibes:
|
|
918
|
+
from rem.agentic.providers.phoenix import validate_evaluator_credentials
|
|
919
|
+
|
|
920
|
+
click.echo("Validating evaluator credentials...")
|
|
921
|
+
is_valid, error_msg = validate_evaluator_credentials()
|
|
922
|
+
if not is_valid:
|
|
923
|
+
click.echo(click.style(f"\n⚠️ Evaluator validation failed: {error_msg}", fg="yellow"))
|
|
924
|
+
click.echo("\nOptions:")
|
|
925
|
+
click.echo(" 1. Fix the credentials issue and re-run")
|
|
926
|
+
click.echo(" 2. Run with --only-vibes to skip LLM evaluation")
|
|
927
|
+
click.echo(" 3. Use --evaluator-model to specify a different model")
|
|
928
|
+
raise click.Abort()
|
|
929
|
+
click.echo("✓ Evaluator credentials validated")
|
|
930
|
+
|
|
931
|
+
# Load dataset using read_dataframe utility (auto-detects format from extension)
|
|
932
|
+
from rem.utils.files import read_dataframe
|
|
920
933
|
|
|
921
934
|
click.echo(f"Loading dataset: {list(config.datasets.keys())[0]}")
|
|
922
935
|
dataset_ref = list(config.datasets.values())[0]
|
|
923
936
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
937
|
+
try:
|
|
938
|
+
if dataset_ref.location.value == "git":
|
|
939
|
+
# Load from Git (local filesystem)
|
|
940
|
+
dataset_path = Path(base_path) / name / dataset_ref.path
|
|
941
|
+
if not dataset_path.exists():
|
|
942
|
+
click.echo(f"Error: Dataset not found: {dataset_path}")
|
|
943
|
+
raise click.Abort()
|
|
930
944
|
|
|
931
|
-
|
|
932
|
-
dataset_df = pl.read_csv(dataset_path)
|
|
933
|
-
elif dataset_ref.format == "parquet":
|
|
934
|
-
dataset_df = pl.read_parquet(dataset_path)
|
|
935
|
-
elif dataset_ref.format == "jsonl":
|
|
936
|
-
dataset_df = pl.read_ndjson(dataset_path)
|
|
937
|
-
else:
|
|
938
|
-
click.echo(f"Error: Format '{dataset_ref.format}' not yet supported")
|
|
939
|
-
raise click.Abort()
|
|
940
|
-
elif dataset_ref.location.value in ["s3", "hybrid"]:
|
|
941
|
-
# Load from S3 using FS provider
|
|
942
|
-
from rem.services.fs import FS
|
|
943
|
-
from io import BytesIO
|
|
945
|
+
dataset_df = read_dataframe(dataset_path)
|
|
944
946
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if dataset_ref.format == "csv":
|
|
949
|
-
content = fs.read(dataset_ref.path)
|
|
950
|
-
dataset_df = pl.read_csv(BytesIO(content.encode() if isinstance(content, str) else content))
|
|
951
|
-
elif dataset_ref.format == "parquet":
|
|
952
|
-
content_bytes = fs.read(dataset_ref.path)
|
|
953
|
-
dataset_df = pl.read_parquet(BytesIO(content_bytes if isinstance(content_bytes, bytes) else content_bytes.encode()))
|
|
954
|
-
elif dataset_ref.format == "jsonl":
|
|
955
|
-
content = fs.read(dataset_ref.path)
|
|
956
|
-
dataset_df = pl.read_ndjson(BytesIO(content.encode() if isinstance(content, str) else content))
|
|
957
|
-
else:
|
|
958
|
-
click.echo(f"Error: Format '{dataset_ref.format}' not yet supported")
|
|
959
|
-
raise click.Abort()
|
|
947
|
+
elif dataset_ref.location.value in ["s3", "hybrid"]:
|
|
948
|
+
# Load from S3 using FS provider
|
|
949
|
+
from rem.services.fs import FS
|
|
960
950
|
|
|
951
|
+
fs = FS()
|
|
952
|
+
content = fs.read(dataset_ref.path)
|
|
953
|
+
# Ensure we have bytes
|
|
954
|
+
if isinstance(content, str):
|
|
955
|
+
content = content.encode()
|
|
956
|
+
dataset_df = read_dataframe(content, filename=dataset_ref.path)
|
|
961
957
|
click.echo(f"✓ Loaded dataset from S3")
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
click.echo(f"Error:
|
|
965
|
-
click.echo(f" Path: {dataset_ref.path}")
|
|
966
|
-
click.echo(f" Format: {dataset_ref.format}")
|
|
958
|
+
|
|
959
|
+
else:
|
|
960
|
+
click.echo(f"Error: Unknown dataset location: {dataset_ref.location.value}")
|
|
967
961
|
raise click.Abort()
|
|
968
|
-
|
|
969
|
-
|
|
962
|
+
|
|
963
|
+
except ValueError as e:
|
|
964
|
+
# Unsupported format error from read_dataframe
|
|
965
|
+
click.echo(f"Error: {e}")
|
|
966
|
+
raise click.Abort()
|
|
967
|
+
except Exception as e:
|
|
968
|
+
logger.error(f"Failed to load dataset: {e}")
|
|
969
|
+
click.echo(f"Error: Could not load dataset")
|
|
970
|
+
click.echo(f" Path: {dataset_ref.path}")
|
|
970
971
|
raise click.Abort()
|
|
971
972
|
|
|
972
973
|
click.echo(f"✓ Loaded dataset: {len(dataset_df)} examples")
|
|
@@ -1286,7 +1287,7 @@ def prompt():
|
|
|
1286
1287
|
@click.option("--system-prompt", "-s", required=True, help="System prompt text")
|
|
1287
1288
|
@click.option("--description", "-d", help="Prompt description")
|
|
1288
1289
|
@click.option("--model-provider", default="OPENAI", help="Model provider (OPENAI, ANTHROPIC)")
|
|
1289
|
-
@click.option("--model-name", "-m", help="Model name (e.g., gpt-
|
|
1290
|
+
@click.option("--model-name", "-m", help="Model name (e.g., gpt-4.1, claude-sonnet-4-5)")
|
|
1290
1291
|
@click.option("--type", "-t", "prompt_type", default="Agent", help="Prompt type (Agent or Evaluator)")
|
|
1291
1292
|
def prompt_create(
|
|
1292
1293
|
name: str,
|
|
@@ -1302,7 +1303,7 @@ def prompt_create(
|
|
|
1302
1303
|
# Create agent prompt
|
|
1303
1304
|
rem experiments prompt create hello-world \\
|
|
1304
1305
|
--system-prompt "You are a helpful assistant." \\
|
|
1305
|
-
--model-name gpt-
|
|
1306
|
+
--model-name gpt-4.1
|
|
1306
1307
|
|
|
1307
1308
|
# Create evaluator prompt
|
|
1308
1309
|
rem experiments prompt create correctness-evaluator \\
|
|
@@ -1320,7 +1321,7 @@ def prompt_create(
|
|
|
1320
1321
|
try:
|
|
1321
1322
|
# Set default model if not specified
|
|
1322
1323
|
if not model_name:
|
|
1323
|
-
model_name = "gpt-
|
|
1324
|
+
model_name = "gpt-4.1" if model_provider == "OPENAI" else "claude-sonnet-4-5-20250929"
|
|
1324
1325
|
|
|
1325
1326
|
# Get config
|
|
1326
1327
|
phoenix_client = PhoenixClient()
|