vibesurf 0.1.0__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 vibesurf might be problematic. Click here for more details.
- vibe_surf/__init__.py +12 -0
- vibe_surf/_version.py +34 -0
- vibe_surf/agents/__init__.py +0 -0
- vibe_surf/agents/browser_use_agent.py +1106 -0
- vibe_surf/agents/prompts/__init__.py +1 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +176 -0
- vibe_surf/agents/report_writer_agent.py +360 -0
- vibe_surf/agents/vibe_surf_agent.py +1632 -0
- vibe_surf/backend/__init__.py +0 -0
- vibe_surf/backend/api/__init__.py +3 -0
- vibe_surf/backend/api/activity.py +243 -0
- vibe_surf/backend/api/config.py +740 -0
- vibe_surf/backend/api/files.py +322 -0
- vibe_surf/backend/api/models.py +257 -0
- vibe_surf/backend/api/task.py +300 -0
- vibe_surf/backend/database/__init__.py +13 -0
- vibe_surf/backend/database/manager.py +129 -0
- vibe_surf/backend/database/models.py +164 -0
- vibe_surf/backend/database/queries.py +922 -0
- vibe_surf/backend/database/schemas.py +100 -0
- vibe_surf/backend/llm_config.py +182 -0
- vibe_surf/backend/main.py +137 -0
- vibe_surf/backend/migrations/__init__.py +16 -0
- vibe_surf/backend/migrations/init_db.py +303 -0
- vibe_surf/backend/migrations/seed_data.py +236 -0
- vibe_surf/backend/shared_state.py +601 -0
- vibe_surf/backend/utils/__init__.py +7 -0
- vibe_surf/backend/utils/encryption.py +164 -0
- vibe_surf/backend/utils/llm_factory.py +225 -0
- vibe_surf/browser/__init__.py +8 -0
- vibe_surf/browser/agen_browser_profile.py +130 -0
- vibe_surf/browser/agent_browser_session.py +416 -0
- vibe_surf/browser/browser_manager.py +296 -0
- vibe_surf/browser/utils.py +790 -0
- vibe_surf/browser/watchdogs/__init__.py +0 -0
- vibe_surf/browser/watchdogs/action_watchdog.py +291 -0
- vibe_surf/browser/watchdogs/dom_watchdog.py +954 -0
- vibe_surf/chrome_extension/background.js +558 -0
- vibe_surf/chrome_extension/config.js +48 -0
- vibe_surf/chrome_extension/content.js +284 -0
- vibe_surf/chrome_extension/dev-reload.js +47 -0
- vibe_surf/chrome_extension/icons/convert-svg.js +33 -0
- vibe_surf/chrome_extension/icons/logo-preview.html +187 -0
- vibe_surf/chrome_extension/icons/logo.png +0 -0
- vibe_surf/chrome_extension/manifest.json +53 -0
- vibe_surf/chrome_extension/popup.html +134 -0
- vibe_surf/chrome_extension/scripts/api-client.js +473 -0
- vibe_surf/chrome_extension/scripts/main.js +491 -0
- vibe_surf/chrome_extension/scripts/markdown-it.min.js +3 -0
- vibe_surf/chrome_extension/scripts/session-manager.js +599 -0
- vibe_surf/chrome_extension/scripts/ui-manager.js +3687 -0
- vibe_surf/chrome_extension/sidepanel.html +347 -0
- vibe_surf/chrome_extension/styles/animations.css +471 -0
- vibe_surf/chrome_extension/styles/components.css +670 -0
- vibe_surf/chrome_extension/styles/main.css +2307 -0
- vibe_surf/chrome_extension/styles/settings.css +1100 -0
- vibe_surf/cli.py +357 -0
- vibe_surf/controller/__init__.py +0 -0
- vibe_surf/controller/file_system.py +53 -0
- vibe_surf/controller/mcp_client.py +68 -0
- vibe_surf/controller/vibesurf_controller.py +616 -0
- vibe_surf/controller/views.py +37 -0
- vibe_surf/llm/__init__.py +21 -0
- vibe_surf/llm/openai_compatible.py +237 -0
- vibesurf-0.1.0.dist-info/METADATA +97 -0
- vibesurf-0.1.0.dist-info/RECORD +70 -0
- vibesurf-0.1.0.dist-info/WHEEL +5 -0
- vibesurf-0.1.0.dist-info/entry_points.txt +2 -0
- vibesurf-0.1.0.dist-info/licenses/LICENSE +201 -0
- vibesurf-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration API endpoints for VibeSurf Backend
|
|
3
|
+
|
|
4
|
+
Handles LLM Profile and controller configuration management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Depends
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
from typing import Dict, List
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from ..database.manager import get_db_session
|
|
14
|
+
from ..database.queries import LLMProfileQueries, McpProfileQueries
|
|
15
|
+
from .models import (
|
|
16
|
+
LLMProfileCreateRequest, LLMProfileUpdateRequest, LLMProfileResponse,
|
|
17
|
+
McpProfileCreateRequest, McpProfileUpdateRequest, McpProfileResponse
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
router = APIRouter(prefix="/config", tags=["config"])
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
def _profile_to_response_dict(profile) -> dict:
|
|
24
|
+
"""Convert SQLAlchemy LLMProfile to dict for Pydantic validation - safe extraction"""
|
|
25
|
+
try:
|
|
26
|
+
# Use SQLAlchemy's __dict__ to avoid lazy loading issues
|
|
27
|
+
profile_dict = profile.__dict__.copy()
|
|
28
|
+
|
|
29
|
+
# Remove SQLAlchemy internal keys
|
|
30
|
+
profile_dict.pop('_sa_instance_state', None)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
"profile_id": profile_dict.get("profile_id"),
|
|
34
|
+
"profile_name": profile_dict.get("profile_name"),
|
|
35
|
+
"provider": profile_dict.get("provider"),
|
|
36
|
+
"model": profile_dict.get("model"),
|
|
37
|
+
"base_url": profile_dict.get("base_url"),
|
|
38
|
+
"temperature": profile_dict.get("temperature"),
|
|
39
|
+
"max_tokens": profile_dict.get("max_tokens"),
|
|
40
|
+
"top_p": profile_dict.get("top_p"),
|
|
41
|
+
"frequency_penalty": profile_dict.get("frequency_penalty"),
|
|
42
|
+
"seed": profile_dict.get("seed"),
|
|
43
|
+
"provider_config": profile_dict.get("provider_config"),
|
|
44
|
+
"description": profile_dict.get("description"),
|
|
45
|
+
"is_active": profile_dict.get("is_active"),
|
|
46
|
+
"is_default": profile_dict.get("is_default"),
|
|
47
|
+
"created_at": profile_dict.get("created_at"),
|
|
48
|
+
"updated_at": profile_dict.get("updated_at"),
|
|
49
|
+
"last_used_at": profile_dict.get("last_used_at")
|
|
50
|
+
}
|
|
51
|
+
except Exception as e:
|
|
52
|
+
# Fallback to direct attribute access if __dict__ approach fails
|
|
53
|
+
return {
|
|
54
|
+
"profile_id": str(profile.profile_id),
|
|
55
|
+
"profile_name": str(profile.profile_name),
|
|
56
|
+
"provider": str(profile.provider),
|
|
57
|
+
"model": str(profile.model),
|
|
58
|
+
"base_url": profile.base_url,
|
|
59
|
+
"temperature": profile.temperature,
|
|
60
|
+
"max_tokens": profile.max_tokens,
|
|
61
|
+
"top_p": profile.top_p,
|
|
62
|
+
"frequency_penalty": profile.frequency_penalty,
|
|
63
|
+
"seed": profile.seed,
|
|
64
|
+
"provider_config": profile.provider_config or {},
|
|
65
|
+
"description": profile.description,
|
|
66
|
+
"is_active": bool(profile.is_active),
|
|
67
|
+
"is_default": bool(profile.is_default),
|
|
68
|
+
"created_at": profile.created_at,
|
|
69
|
+
"updated_at": profile.updated_at,
|
|
70
|
+
"last_used_at": profile.last_used_at
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# LLM Profile Management
|
|
74
|
+
@router.post("/llm-profiles", response_model=LLMProfileResponse)
|
|
75
|
+
async def create_llm_profile(
|
|
76
|
+
profile_request: LLMProfileCreateRequest,
|
|
77
|
+
db: AsyncSession = Depends(get_db_session)
|
|
78
|
+
):
|
|
79
|
+
"""Create a new LLM profile"""
|
|
80
|
+
try:
|
|
81
|
+
# Check if profile name already exists
|
|
82
|
+
existing_profile = await LLMProfileQueries.get_profile(db, profile_request.profile_name)
|
|
83
|
+
if existing_profile:
|
|
84
|
+
raise HTTPException(
|
|
85
|
+
status_code=400,
|
|
86
|
+
detail=f"Profile with name '{profile_request.profile_name}' already exists"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Create new profile - now returns dict directly
|
|
90
|
+
profile_data = await LLMProfileQueries.create_profile(
|
|
91
|
+
db=db,
|
|
92
|
+
profile_name=profile_request.profile_name,
|
|
93
|
+
provider=profile_request.provider,
|
|
94
|
+
model=profile_request.model,
|
|
95
|
+
api_key=profile_request.api_key,
|
|
96
|
+
base_url=profile_request.base_url,
|
|
97
|
+
temperature=profile_request.temperature,
|
|
98
|
+
max_tokens=profile_request.max_tokens,
|
|
99
|
+
top_p=profile_request.top_p,
|
|
100
|
+
frequency_penalty=profile_request.frequency_penalty,
|
|
101
|
+
seed=profile_request.seed,
|
|
102
|
+
provider_config=profile_request.provider_config,
|
|
103
|
+
description=profile_request.description,
|
|
104
|
+
is_default=profile_request.is_default
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
await db.commit()
|
|
108
|
+
|
|
109
|
+
# If this is set as default, update other profiles
|
|
110
|
+
if profile_request.is_default:
|
|
111
|
+
await LLMProfileQueries.set_default_profile(db, profile_request.profile_name)
|
|
112
|
+
await db.commit()
|
|
113
|
+
|
|
114
|
+
return LLMProfileResponse(**profile_data)
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Failed to create LLM profile: {e}")
|
|
118
|
+
raise HTTPException(
|
|
119
|
+
status_code=500,
|
|
120
|
+
detail=f"Failed to create LLM profile: {str(e)}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@router.get("/llm-profiles", response_model=List[LLMProfileResponse])
|
|
124
|
+
async def list_llm_profiles(
|
|
125
|
+
active_only: bool = True,
|
|
126
|
+
limit: int = 50,
|
|
127
|
+
offset: int = 0,
|
|
128
|
+
db: AsyncSession = Depends(get_db_session)
|
|
129
|
+
):
|
|
130
|
+
"""List LLM profiles"""
|
|
131
|
+
try:
|
|
132
|
+
profiles = await LLMProfileQueries.list_profiles(
|
|
133
|
+
db=db,
|
|
134
|
+
active_only=active_only,
|
|
135
|
+
limit=limit,
|
|
136
|
+
offset=offset
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Use safe extraction to avoid greenlet issues
|
|
140
|
+
return [LLMProfileResponse(**_profile_to_response_dict(profile)) for profile in profiles]
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Failed to list LLM profiles: {e}")
|
|
144
|
+
raise HTTPException(
|
|
145
|
+
status_code=500,
|
|
146
|
+
detail=f"Failed to list LLM profiles: {str(e)}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@router.get("/llm-profiles/{profile_name}", response_model=LLMProfileResponse)
|
|
150
|
+
async def get_llm_profile(
|
|
151
|
+
profile_name: str,
|
|
152
|
+
db: AsyncSession = Depends(get_db_session)
|
|
153
|
+
):
|
|
154
|
+
"""Get specific LLM profile by name"""
|
|
155
|
+
try:
|
|
156
|
+
profile = await LLMProfileQueries.get_profile(db, profile_name)
|
|
157
|
+
if not profile:
|
|
158
|
+
raise HTTPException(
|
|
159
|
+
status_code=404,
|
|
160
|
+
detail=f"LLM profile '{profile_name}' not found"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Use safe extraction to avoid greenlet issues
|
|
164
|
+
return LLMProfileResponse(**_profile_to_response_dict(profile))
|
|
165
|
+
|
|
166
|
+
except HTTPException:
|
|
167
|
+
raise
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"Failed to get LLM profile: {e}")
|
|
170
|
+
raise HTTPException(
|
|
171
|
+
status_code=500,
|
|
172
|
+
detail=f"Failed to get LLM profile: {str(e)}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@router.put("/llm-profiles/{profile_name}", response_model=LLMProfileResponse)
|
|
176
|
+
async def update_llm_profile(
|
|
177
|
+
profile_name: str,
|
|
178
|
+
update_request: LLMProfileUpdateRequest,
|
|
179
|
+
db: AsyncSession = Depends(get_db_session)
|
|
180
|
+
):
|
|
181
|
+
"""Update an existing LLM profile"""
|
|
182
|
+
try:
|
|
183
|
+
# Check if profile exists
|
|
184
|
+
existing_profile = await LLMProfileQueries.get_profile(db, profile_name)
|
|
185
|
+
if not existing_profile:
|
|
186
|
+
raise HTTPException(
|
|
187
|
+
status_code=404,
|
|
188
|
+
detail=f"LLM profile '{profile_name}' not found"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Prepare update data
|
|
192
|
+
update_data = {}
|
|
193
|
+
for field, value in update_request.model_dump(exclude_unset=True).items():
|
|
194
|
+
if value is not None:
|
|
195
|
+
update_data[field] = value
|
|
196
|
+
|
|
197
|
+
if not update_data:
|
|
198
|
+
raise HTTPException(
|
|
199
|
+
status_code=400,
|
|
200
|
+
detail="No valid fields provided for update"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Update profile
|
|
204
|
+
success = await LLMProfileQueries.update_profile(db, profile_name, update_data)
|
|
205
|
+
if not success:
|
|
206
|
+
raise HTTPException(
|
|
207
|
+
status_code=500,
|
|
208
|
+
detail="Failed to update profile"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
await db.commit()
|
|
212
|
+
|
|
213
|
+
# Handle default profile setting
|
|
214
|
+
if update_request.is_default:
|
|
215
|
+
await LLMProfileQueries.set_default_profile(db, profile_name)
|
|
216
|
+
await db.commit()
|
|
217
|
+
|
|
218
|
+
# Return updated profile
|
|
219
|
+
updated_profile = await LLMProfileQueries.get_profile(db, profile_name)
|
|
220
|
+
|
|
221
|
+
# Use safe extraction to avoid greenlet issues
|
|
222
|
+
return LLMProfileResponse(**_profile_to_response_dict(updated_profile))
|
|
223
|
+
|
|
224
|
+
except HTTPException:
|
|
225
|
+
raise
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Failed to update LLM profile: {e}")
|
|
228
|
+
raise HTTPException(
|
|
229
|
+
status_code=500,
|
|
230
|
+
detail=f"Failed to update LLM profile: {str(e)}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@router.delete("/llm-profiles/{profile_name}")
|
|
234
|
+
async def delete_llm_profile(
|
|
235
|
+
profile_name: str,
|
|
236
|
+
db: AsyncSession = Depends(get_db_session)
|
|
237
|
+
):
|
|
238
|
+
"""Delete an LLM profile"""
|
|
239
|
+
try:
|
|
240
|
+
# Check if profile exists
|
|
241
|
+
existing_profile = await LLMProfileQueries.get_profile(db, profile_name)
|
|
242
|
+
if not existing_profile:
|
|
243
|
+
raise HTTPException(
|
|
244
|
+
status_code=404,
|
|
245
|
+
detail=f"LLM profile '{profile_name}' not found"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Don't allow deletion of default profile
|
|
249
|
+
if existing_profile.is_default:
|
|
250
|
+
raise HTTPException(
|
|
251
|
+
status_code=400,
|
|
252
|
+
detail="Cannot delete the default profile. Set another profile as default first."
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# TODO: Check if profile is being used by any active tasks
|
|
256
|
+
# This would require checking the tasks table
|
|
257
|
+
|
|
258
|
+
success = await LLMProfileQueries.delete_profile(db, profile_name)
|
|
259
|
+
if not success:
|
|
260
|
+
raise HTTPException(
|
|
261
|
+
status_code=500,
|
|
262
|
+
detail="Failed to delete profile"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
await db.commit()
|
|
266
|
+
|
|
267
|
+
return JSONResponse(
|
|
268
|
+
content={"message": f"LLM profile '{profile_name}' deleted successfully"},
|
|
269
|
+
status_code=200
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
except HTTPException:
|
|
273
|
+
raise
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Failed to delete LLM profile: {e}")
|
|
276
|
+
raise HTTPException(
|
|
277
|
+
status_code=500,
|
|
278
|
+
detail=f"Failed to delete LLM profile: {str(e)}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
@router.post("/llm-profiles/{profile_name}/set-default")
|
|
282
|
+
async def set_default_llm_profile(
|
|
283
|
+
profile_name: str,
|
|
284
|
+
db: AsyncSession = Depends(get_db_session)
|
|
285
|
+
):
|
|
286
|
+
"""Set an LLM profile as the default"""
|
|
287
|
+
try:
|
|
288
|
+
# Check if profile exists and is active
|
|
289
|
+
profile = await LLMProfileQueries.get_profile(db, profile_name)
|
|
290
|
+
if not profile:
|
|
291
|
+
raise HTTPException(
|
|
292
|
+
status_code=404,
|
|
293
|
+
detail=f"LLM profile '{profile_name}' not found"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if not profile.is_active:
|
|
297
|
+
raise HTTPException(
|
|
298
|
+
status_code=400,
|
|
299
|
+
detail="Cannot set inactive profile as default"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
success = await LLMProfileQueries.set_default_profile(db, profile_name)
|
|
303
|
+
if not success:
|
|
304
|
+
raise HTTPException(
|
|
305
|
+
status_code=500,
|
|
306
|
+
detail="Failed to set default profile"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
await db.commit()
|
|
310
|
+
|
|
311
|
+
return JSONResponse(
|
|
312
|
+
content={"message": f"LLM profile '{profile_name}' set as default"},
|
|
313
|
+
status_code=200
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except HTTPException:
|
|
317
|
+
raise
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(f"Failed to set default LLM profile: {e}")
|
|
320
|
+
raise HTTPException(
|
|
321
|
+
status_code=500,
|
|
322
|
+
detail=f"Failed to set default LLM profile: {str(e)}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
@router.get("/llm-profiles/default/current", response_model=LLMProfileResponse)
|
|
326
|
+
async def get_default_llm_profile(db: AsyncSession = Depends(get_db_session)):
|
|
327
|
+
"""Get the current default LLM profile"""
|
|
328
|
+
try:
|
|
329
|
+
profile = await LLMProfileQueries.get_default_profile(db)
|
|
330
|
+
if not profile:
|
|
331
|
+
raise HTTPException(
|
|
332
|
+
status_code=404,
|
|
333
|
+
detail="No default LLM profile found"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Use safe extraction to avoid greenlet issues
|
|
337
|
+
return LLMProfileResponse(**_profile_to_response_dict(profile))
|
|
338
|
+
|
|
339
|
+
except HTTPException:
|
|
340
|
+
raise
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(f"Failed to get default LLM profile: {e}")
|
|
343
|
+
raise HTTPException(
|
|
344
|
+
status_code=500,
|
|
345
|
+
detail=f"Failed to get default LLM profile: {str(e)}"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# MCP Profile Management
|
|
349
|
+
def _mcp_profile_to_response_dict(profile) -> dict:
|
|
350
|
+
"""Convert SQLAlchemy McpProfile to dict for Pydantic validation - safe extraction"""
|
|
351
|
+
try:
|
|
352
|
+
# Use SQLAlchemy's __dict__ to avoid lazy loading issues
|
|
353
|
+
profile_dict = profile.__dict__.copy()
|
|
354
|
+
|
|
355
|
+
# Remove SQLAlchemy internal keys
|
|
356
|
+
profile_dict.pop('_sa_instance_state', None)
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
"mcp_id": profile_dict.get("mcp_id"),
|
|
360
|
+
"display_name": profile_dict.get("display_name"),
|
|
361
|
+
"mcp_server_name": profile_dict.get("mcp_server_name"),
|
|
362
|
+
"mcp_server_params": profile_dict.get("mcp_server_params"),
|
|
363
|
+
"description": profile_dict.get("description"),
|
|
364
|
+
"is_active": profile_dict.get("is_active"),
|
|
365
|
+
"created_at": profile_dict.get("created_at"),
|
|
366
|
+
"updated_at": profile_dict.get("updated_at"),
|
|
367
|
+
"last_used_at": profile_dict.get("last_used_at")
|
|
368
|
+
}
|
|
369
|
+
except Exception as e:
|
|
370
|
+
# Fallback to direct attribute access if __dict__ approach fails
|
|
371
|
+
return {
|
|
372
|
+
"mcp_id": str(profile.mcp_id),
|
|
373
|
+
"display_name": str(profile.display_name),
|
|
374
|
+
"mcp_server_name": str(profile.mcp_server_name),
|
|
375
|
+
"mcp_server_params": profile.mcp_server_params or {},
|
|
376
|
+
"description": profile.description,
|
|
377
|
+
"is_active": bool(profile.is_active),
|
|
378
|
+
"created_at": profile.created_at,
|
|
379
|
+
"updated_at": profile.updated_at,
|
|
380
|
+
"last_used_at": profile.last_used_at
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@router.post("/mcp-profiles", response_model=McpProfileResponse)
|
|
384
|
+
async def create_mcp_profile(
|
|
385
|
+
profile_request: McpProfileCreateRequest,
|
|
386
|
+
db: AsyncSession = Depends(get_db_session)
|
|
387
|
+
):
|
|
388
|
+
"""Create a new MCP profile"""
|
|
389
|
+
try:
|
|
390
|
+
# Check if display name already exists
|
|
391
|
+
existing_profile = await McpProfileQueries.get_profile_by_display_name(db, profile_request.display_name)
|
|
392
|
+
if existing_profile:
|
|
393
|
+
raise HTTPException(
|
|
394
|
+
status_code=400,
|
|
395
|
+
detail=f"MCP Profile with display name '{profile_request.display_name}' already exists"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Create new profile
|
|
399
|
+
profile_data = await McpProfileQueries.create_profile(
|
|
400
|
+
db=db,
|
|
401
|
+
display_name=profile_request.display_name,
|
|
402
|
+
mcp_server_name=profile_request.mcp_server_name,
|
|
403
|
+
mcp_server_params=profile_request.mcp_server_params,
|
|
404
|
+
description=profile_request.description
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
await db.commit()
|
|
408
|
+
|
|
409
|
+
return McpProfileResponse(**profile_data)
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.error(f"Failed to create MCP profile: {e}")
|
|
413
|
+
raise HTTPException(
|
|
414
|
+
status_code=500,
|
|
415
|
+
detail=f"Failed to create MCP profile: {str(e)}"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
@router.get("/mcp-profiles", response_model=List[McpProfileResponse])
|
|
419
|
+
async def list_mcp_profiles(
|
|
420
|
+
active_only: bool = True,
|
|
421
|
+
limit: int = 50,
|
|
422
|
+
offset: int = 0,
|
|
423
|
+
db: AsyncSession = Depends(get_db_session)
|
|
424
|
+
):
|
|
425
|
+
"""List MCP profiles"""
|
|
426
|
+
try:
|
|
427
|
+
profiles = await McpProfileQueries.list_profiles(
|
|
428
|
+
db=db,
|
|
429
|
+
active_only=active_only,
|
|
430
|
+
limit=limit,
|
|
431
|
+
offset=offset
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return [McpProfileResponse(**_mcp_profile_to_response_dict(profile)) for profile in profiles]
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.error(f"Failed to list MCP profiles: {e}")
|
|
438
|
+
raise HTTPException(
|
|
439
|
+
status_code=500,
|
|
440
|
+
detail=f"Failed to list MCP profiles: {str(e)}"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
@router.get("/mcp-profiles/{mcp_id}", response_model=McpProfileResponse)
|
|
444
|
+
async def get_mcp_profile(
|
|
445
|
+
mcp_id: str,
|
|
446
|
+
db: AsyncSession = Depends(get_db_session)
|
|
447
|
+
):
|
|
448
|
+
"""Get specific MCP profile by ID"""
|
|
449
|
+
try:
|
|
450
|
+
profile = await McpProfileQueries.get_profile(db, mcp_id)
|
|
451
|
+
if not profile:
|
|
452
|
+
raise HTTPException(
|
|
453
|
+
status_code=404,
|
|
454
|
+
detail=f"MCP profile '{mcp_id}' not found"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return McpProfileResponse(**_mcp_profile_to_response_dict(profile))
|
|
458
|
+
|
|
459
|
+
except HTTPException:
|
|
460
|
+
raise
|
|
461
|
+
except Exception as e:
|
|
462
|
+
logger.error(f"Failed to get MCP profile: {e}")
|
|
463
|
+
raise HTTPException(
|
|
464
|
+
status_code=500,
|
|
465
|
+
detail=f"Failed to get MCP profile: {str(e)}"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
@router.put("/mcp-profiles/{mcp_id}", response_model=McpProfileResponse)
|
|
469
|
+
async def update_mcp_profile(
|
|
470
|
+
mcp_id: str,
|
|
471
|
+
update_request: McpProfileUpdateRequest,
|
|
472
|
+
db: AsyncSession = Depends(get_db_session)
|
|
473
|
+
):
|
|
474
|
+
"""Update an existing MCP profile"""
|
|
475
|
+
try:
|
|
476
|
+
logger.info(f"Updating MCP profile {mcp_id}")
|
|
477
|
+
|
|
478
|
+
# Check if profile exists
|
|
479
|
+
existing_profile = await McpProfileQueries.get_profile(db, mcp_id)
|
|
480
|
+
if not existing_profile:
|
|
481
|
+
raise HTTPException(
|
|
482
|
+
status_code=404,
|
|
483
|
+
detail=f"MCP profile '{mcp_id}' not found"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Prepare update data
|
|
487
|
+
update_data = {}
|
|
488
|
+
for field, value in update_request.model_dump(exclude_unset=True).items():
|
|
489
|
+
if value is not None:
|
|
490
|
+
update_data[field] = value
|
|
491
|
+
|
|
492
|
+
if not update_data:
|
|
493
|
+
raise HTTPException(
|
|
494
|
+
status_code=400,
|
|
495
|
+
detail="No valid fields provided for update"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Update profile
|
|
499
|
+
success = await McpProfileQueries.update_profile(db, mcp_id, update_data)
|
|
500
|
+
|
|
501
|
+
if not success:
|
|
502
|
+
raise HTTPException(
|
|
503
|
+
status_code=500,
|
|
504
|
+
detail="Failed to update profile"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
await db.commit()
|
|
508
|
+
|
|
509
|
+
# Return updated profile
|
|
510
|
+
updated_profile = await McpProfileQueries.get_profile(db, mcp_id)
|
|
511
|
+
response_data = _mcp_profile_to_response_dict(updated_profile)
|
|
512
|
+
|
|
513
|
+
return McpProfileResponse(**response_data)
|
|
514
|
+
|
|
515
|
+
except HTTPException:
|
|
516
|
+
raise
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.error(f"Failed to update MCP profile: {e}")
|
|
519
|
+
raise HTTPException(
|
|
520
|
+
status_code=500,
|
|
521
|
+
detail=f"Failed to update MCP profile: {str(e)}"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
@router.delete("/mcp-profiles/{mcp_id}")
|
|
525
|
+
async def delete_mcp_profile(
|
|
526
|
+
mcp_id: str,
|
|
527
|
+
db: AsyncSession = Depends(get_db_session)
|
|
528
|
+
):
|
|
529
|
+
"""Delete an MCP profile"""
|
|
530
|
+
try:
|
|
531
|
+
# Check if profile exists
|
|
532
|
+
existing_profile = await McpProfileQueries.get_profile(db, mcp_id)
|
|
533
|
+
if not existing_profile:
|
|
534
|
+
raise HTTPException(
|
|
535
|
+
status_code=404,
|
|
536
|
+
detail=f"MCP profile '{mcp_id}' not found"
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# TODO: Check if profile is being used by any active tasks
|
|
540
|
+
# This would require checking the tasks table
|
|
541
|
+
|
|
542
|
+
success = await McpProfileQueries.delete_profile(db, mcp_id)
|
|
543
|
+
if not success:
|
|
544
|
+
raise HTTPException(
|
|
545
|
+
status_code=500,
|
|
546
|
+
detail="Failed to delete profile"
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
await db.commit()
|
|
550
|
+
|
|
551
|
+
return JSONResponse(
|
|
552
|
+
content={"message": f"MCP profile '{existing_profile.display_name}' deleted successfully"},
|
|
553
|
+
status_code=200
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
except HTTPException:
|
|
557
|
+
raise
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.error(f"Failed to delete MCP profile: {e}")
|
|
560
|
+
raise HTTPException(
|
|
561
|
+
status_code=500,
|
|
562
|
+
detail=f"Failed to delete MCP profile: {str(e)}"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
@router.get("/llm/providers")
|
|
566
|
+
async def get_available_providers():
|
|
567
|
+
"""Get list of available LLM providers"""
|
|
568
|
+
from ..llm_config import get_supported_providers, get_provider_models, get_provider_metadata
|
|
569
|
+
|
|
570
|
+
providers = []
|
|
571
|
+
for provider_name in get_supported_providers():
|
|
572
|
+
metadata = get_provider_metadata(provider_name)
|
|
573
|
+
models = get_provider_models(provider_name)
|
|
574
|
+
|
|
575
|
+
provider_info = {
|
|
576
|
+
"name": provider_name,
|
|
577
|
+
"display_name": metadata.get("display_name", provider_name.title()),
|
|
578
|
+
"models": models,
|
|
579
|
+
"model_count": len(models),
|
|
580
|
+
"requires_api_key": metadata.get("requires_api_key", True),
|
|
581
|
+
"supports_base_url": metadata.get("supports_base_url", False),
|
|
582
|
+
"requires_base_url": metadata.get("requires_base_url", False),
|
|
583
|
+
"supports_tools": metadata.get("supports_tools", False),
|
|
584
|
+
"supports_vision": metadata.get("supports_vision", False),
|
|
585
|
+
"default_model": metadata.get("default_model", "")
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# Add default base URL if available
|
|
589
|
+
if "default_base_url" in metadata:
|
|
590
|
+
provider_info["default_base_url"] = metadata["default_base_url"]
|
|
591
|
+
if "base_url" in metadata:
|
|
592
|
+
provider_info["base_url"] = metadata["base_url"]
|
|
593
|
+
|
|
594
|
+
providers.append(provider_info)
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
"providers": providers,
|
|
598
|
+
"total_providers": len(providers)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
@router.get("/llm/providers/{provider_name}/models")
|
|
602
|
+
async def get_provider_models_endpoint(provider_name: str):
|
|
603
|
+
"""Get models for a specific LLM provider"""
|
|
604
|
+
from ..llm_config import get_provider_models as get_models, get_provider_metadata, is_provider_supported
|
|
605
|
+
|
|
606
|
+
if not is_provider_supported(provider_name):
|
|
607
|
+
raise HTTPException(
|
|
608
|
+
status_code=404,
|
|
609
|
+
detail=f"Provider '{provider_name}' not found or not supported"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
models = get_models(provider_name)
|
|
613
|
+
metadata = get_provider_metadata(provider_name)
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
"provider": provider_name,
|
|
617
|
+
"display_name": metadata.get("display_name", provider_name.title()),
|
|
618
|
+
"models": models,
|
|
619
|
+
"model_count": len(models),
|
|
620
|
+
"default_model": metadata.get("default_model", ""),
|
|
621
|
+
"metadata": metadata
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
# Configuration status endpoints
|
|
625
|
+
@router.get("/status")
|
|
626
|
+
async def get_configuration_status(db: AsyncSession = Depends(get_db_session)):
|
|
627
|
+
"""Get overall configuration status including LLM profiles"""
|
|
628
|
+
try:
|
|
629
|
+
from .. import shared_state
|
|
630
|
+
|
|
631
|
+
# Get LLM profiles info
|
|
632
|
+
total_profiles = len(await LLMProfileQueries.list_profiles(db, active_only=False))
|
|
633
|
+
active_profiles = len(await LLMProfileQueries.list_profiles(db, active_only=True))
|
|
634
|
+
default_profile = await LLMProfileQueries.get_default_profile(db)
|
|
635
|
+
|
|
636
|
+
status = {
|
|
637
|
+
"llm_profiles": {
|
|
638
|
+
"total_profiles": total_profiles,
|
|
639
|
+
"active_profiles": active_profiles,
|
|
640
|
+
"default_profile": default_profile.profile_name if default_profile else None,
|
|
641
|
+
"has_default": default_profile is not None
|
|
642
|
+
},
|
|
643
|
+
"controller": {
|
|
644
|
+
"initialized": shared_state.controller is not None
|
|
645
|
+
},
|
|
646
|
+
"browser_manager": {
|
|
647
|
+
"initialized": shared_state.browser_manager is not None
|
|
648
|
+
},
|
|
649
|
+
"vibesurf_agent": {
|
|
650
|
+
"initialized": shared_state.vibesurf_agent is not None,
|
|
651
|
+
"workspace_dir": shared_state.workspace_dir
|
|
652
|
+
},
|
|
653
|
+
"overall_status": "ready" if (
|
|
654
|
+
default_profile and
|
|
655
|
+
shared_state.controller and
|
|
656
|
+
shared_state.browser_manager and
|
|
657
|
+
shared_state.vibesurf_agent
|
|
658
|
+
) else "partial"
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return status
|
|
662
|
+
|
|
663
|
+
except Exception as e:
|
|
664
|
+
logger.error(f"Failed to get configuration status: {e}")
|
|
665
|
+
raise HTTPException(
|
|
666
|
+
status_code=500,
|
|
667
|
+
detail=f"Failed to get configuration status: {str(e)}"
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# Environment Variables Management
|
|
671
|
+
@router.get("/environments")
|
|
672
|
+
async def get_environments():
|
|
673
|
+
"""Get current environment variables"""
|
|
674
|
+
try:
|
|
675
|
+
from .. import shared_state
|
|
676
|
+
envs = shared_state.get_envs()
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
"environments": envs,
|
|
680
|
+
"count": len(envs)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
logger.error(f"Failed to get environments: {e}")
|
|
685
|
+
raise HTTPException(
|
|
686
|
+
status_code=500,
|
|
687
|
+
detail=f"Failed to get environments: {str(e)}"
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
@router.put("/environments")
|
|
691
|
+
async def update_environments(updates: Dict[str, str]):
|
|
692
|
+
"""Update environment variables"""
|
|
693
|
+
try:
|
|
694
|
+
from .. import shared_state
|
|
695
|
+
|
|
696
|
+
# Validate that we only update allowed keys
|
|
697
|
+
allowed_keys = {
|
|
698
|
+
"BROWSER_EXECUTION_PATH",
|
|
699
|
+
"BROWSER_USER_DATA",
|
|
700
|
+
"VIBESURF_EXTENSION",
|
|
701
|
+
"VIBESURF_BACKEND_URL"
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
# Filter updates to only include allowed keys
|
|
705
|
+
filtered_updates = {
|
|
706
|
+
key: value for key, value in updates.items()
|
|
707
|
+
if key in allowed_keys
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if not filtered_updates:
|
|
711
|
+
raise HTTPException(
|
|
712
|
+
status_code=400,
|
|
713
|
+
detail=f"No valid environment variables provided. Allowed keys: {list(allowed_keys)}"
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# Update environment variables
|
|
717
|
+
success = shared_state.update_envs(filtered_updates)
|
|
718
|
+
if not success:
|
|
719
|
+
raise HTTPException(
|
|
720
|
+
status_code=500,
|
|
721
|
+
detail="Failed to update environment variables"
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# Return updated environments
|
|
725
|
+
updated_envs = shared_state.get_envs()
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
"message": "Environment variables updated successfully",
|
|
729
|
+
"updated_keys": list(filtered_updates.keys()),
|
|
730
|
+
"environments": updated_envs
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
except HTTPException:
|
|
734
|
+
raise
|
|
735
|
+
except Exception as e:
|
|
736
|
+
logger.error(f"Failed to update environments: {e}")
|
|
737
|
+
raise HTTPException(
|
|
738
|
+
status_code=500,
|
|
739
|
+
detail=f"Failed to update environments: {str(e)}"
|
|
740
|
+
)
|