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,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VibeSurf Agent Execution Router
|
|
3
|
+
|
|
4
|
+
Handles task submission, execution control (pause/resume/stop), and status monitoring
|
|
5
|
+
for VibeSurf agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
from typing import List, Optional, Dict, Any
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from uuid_extensions import uuid7str
|
|
15
|
+
|
|
16
|
+
from ..database import get_db_session
|
|
17
|
+
from .models import TaskCreateRequest, TaskControlRequest
|
|
18
|
+
|
|
19
|
+
# Import global variables and functions from shared_state
|
|
20
|
+
from ..shared_state import (
|
|
21
|
+
execute_task_background,
|
|
22
|
+
is_task_running,
|
|
23
|
+
get_active_task_info,
|
|
24
|
+
clear_active_task
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("/status")
|
|
33
|
+
async def check_task_status():
|
|
34
|
+
"""Quick check if a task is currently running"""
|
|
35
|
+
return {
|
|
36
|
+
"has_active_task": is_task_running(),
|
|
37
|
+
"active_task": get_active_task_info()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.post("/submit")
|
|
42
|
+
async def submit_task(
|
|
43
|
+
task_request: "TaskCreateRequest",
|
|
44
|
+
background_tasks: BackgroundTasks,
|
|
45
|
+
db: AsyncSession = Depends(get_db_session)
|
|
46
|
+
):
|
|
47
|
+
"""Submit new task for execution (single task mode)"""
|
|
48
|
+
from ..database.queries import LLMProfileQueries
|
|
49
|
+
from ..shared_state import workspace_dir, active_task
|
|
50
|
+
|
|
51
|
+
# Check if task is already running
|
|
52
|
+
if is_task_running():
|
|
53
|
+
current_task = get_active_task_info()
|
|
54
|
+
return {
|
|
55
|
+
"success": False,
|
|
56
|
+
"message": "Cannot submit task: another task is currently running",
|
|
57
|
+
"active_task": {
|
|
58
|
+
"task_id": current_task.get("task_id"),
|
|
59
|
+
"status": current_task.get("status"),
|
|
60
|
+
"session_id": current_task.get("session_id"),
|
|
61
|
+
"start_time": current_task.get("start_time").isoformat() if current_task.get("start_time") else None
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Get LLM profile from database
|
|
67
|
+
llm_profile = await LLMProfileQueries.get_profile_with_decrypted_key(db, task_request.llm_profile_name)
|
|
68
|
+
if not llm_profile:
|
|
69
|
+
raise HTTPException(
|
|
70
|
+
status_code=404,
|
|
71
|
+
detail=f"LLM profile '{task_request.llm_profile_name}' not found"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Generate task ID
|
|
75
|
+
task_id = uuid7str()
|
|
76
|
+
|
|
77
|
+
# Get MCP server config for saving
|
|
78
|
+
from ..shared_state import controller, active_mcp_server
|
|
79
|
+
mcp_server_config = task_request.mcp_server_config
|
|
80
|
+
if not mcp_server_config and controller and hasattr(controller, 'mcp_server_config'):
|
|
81
|
+
mcp_server_config = controller.mcp_server_config
|
|
82
|
+
|
|
83
|
+
# DEBUG: Log the type and content of mcp_server_config
|
|
84
|
+
logger.info(f"mcp_server_config type: {type(mcp_server_config)}, value: {mcp_server_config}")
|
|
85
|
+
|
|
86
|
+
# Create initial task record in database
|
|
87
|
+
from ..database.queries import TaskQueries
|
|
88
|
+
await TaskQueries.save_task(
|
|
89
|
+
db,
|
|
90
|
+
task_id=task_id,
|
|
91
|
+
session_id=task_request.session_id,
|
|
92
|
+
task_description=task_request.task_description,
|
|
93
|
+
upload_files_path=task_request.upload_files_path,
|
|
94
|
+
mcp_server_config=mcp_server_config,
|
|
95
|
+
llm_profile_name=task_request.llm_profile_name,
|
|
96
|
+
workspace_dir=workspace_dir,
|
|
97
|
+
task_status="pending"
|
|
98
|
+
)
|
|
99
|
+
await db.commit()
|
|
100
|
+
|
|
101
|
+
# Initialize LLM for this task if needed
|
|
102
|
+
if not active_task or active_task["llm_profile_name"] != task_request.llm_profile_name:
|
|
103
|
+
await _ensure_llm_initialized(llm_profile)
|
|
104
|
+
|
|
105
|
+
# Add background task
|
|
106
|
+
background_tasks.add_task(
|
|
107
|
+
execute_task_background,
|
|
108
|
+
task_id=task_id,
|
|
109
|
+
session_id=task_request.session_id,
|
|
110
|
+
task=task_request.task_description,
|
|
111
|
+
llm_profile_name=task_request.llm_profile_name,
|
|
112
|
+
upload_files=task_request.upload_files_path,
|
|
113
|
+
db_session=db
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"success": True,
|
|
118
|
+
"task_id": task_id,
|
|
119
|
+
"session_id": task_request.session_id,
|
|
120
|
+
"status": "submitted",
|
|
121
|
+
"message": "Task submitted for execution",
|
|
122
|
+
"llm_profile": task_request.llm_profile_name,
|
|
123
|
+
"workspace_dir": workspace_dir
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
except HTTPException:
|
|
127
|
+
raise
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Failed to submit task: {e}")
|
|
130
|
+
raise HTTPException(status_code=500, detail=f"Failed to submit task: {str(e)}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _ensure_llm_initialized(llm_profile):
|
|
134
|
+
"""Ensure LLM is initialized with the specified profile"""
|
|
135
|
+
from ..utils.llm_factory import create_llm_from_profile
|
|
136
|
+
from ..shared_state import vibesurf_agent
|
|
137
|
+
|
|
138
|
+
if not vibesurf_agent:
|
|
139
|
+
raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
|
|
140
|
+
|
|
141
|
+
# Always create new LLM instance to ensure we're using the right profile
|
|
142
|
+
new_llm = create_llm_from_profile(llm_profile)
|
|
143
|
+
|
|
144
|
+
# Update vibesurf agent's LLM
|
|
145
|
+
vibesurf_agent.llm = new_llm
|
|
146
|
+
logger.info(f"LLM updated for profile: {llm_profile['profile_name']}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@router.post("/pause")
|
|
150
|
+
async def pause_task(control_request: TaskControlRequest):
|
|
151
|
+
"""Pause current task execution"""
|
|
152
|
+
from ..shared_state import vibesurf_agent
|
|
153
|
+
|
|
154
|
+
if not vibesurf_agent:
|
|
155
|
+
raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
|
|
156
|
+
|
|
157
|
+
if not is_task_running():
|
|
158
|
+
raise HTTPException(status_code=400, detail="No active task to pause")
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
result = await vibesurf_agent.pause(control_request.reason)
|
|
162
|
+
|
|
163
|
+
if result.success:
|
|
164
|
+
# Update active task status
|
|
165
|
+
current_task = get_active_task_info()
|
|
166
|
+
if current_task:
|
|
167
|
+
from ..shared_state import active_task
|
|
168
|
+
active_task["status"] = "paused"
|
|
169
|
+
active_task["pause_reason"] = control_request.reason
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"success": True,
|
|
173
|
+
"message": result.message,
|
|
174
|
+
"operation": "pause"
|
|
175
|
+
}
|
|
176
|
+
else:
|
|
177
|
+
raise HTTPException(status_code=500, detail=result.message)
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f"Failed to pause task: {e}")
|
|
181
|
+
raise HTTPException(status_code=500, detail=f"Failed to pause task: {str(e)}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@router.post("/resume")
|
|
185
|
+
async def resume_task(control_request: TaskControlRequest):
|
|
186
|
+
"""Resume current task execution"""
|
|
187
|
+
from ..shared_state import vibesurf_agent
|
|
188
|
+
|
|
189
|
+
if not vibesurf_agent:
|
|
190
|
+
raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
|
|
191
|
+
|
|
192
|
+
current_task = get_active_task_info()
|
|
193
|
+
if not current_task or current_task.get("status") != "paused":
|
|
194
|
+
raise HTTPException(status_code=400, detail="No paused task to resume")
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
result = await vibesurf_agent.resume(control_request.reason)
|
|
198
|
+
|
|
199
|
+
if result.success:
|
|
200
|
+
# Update active task status
|
|
201
|
+
from ..shared_state import active_task
|
|
202
|
+
active_task["status"] = "running"
|
|
203
|
+
active_task["resume_reason"] = control_request.reason
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"success": True,
|
|
207
|
+
"message": result.message,
|
|
208
|
+
"operation": "resume"
|
|
209
|
+
}
|
|
210
|
+
else:
|
|
211
|
+
raise HTTPException(status_code=500, detail=result.message)
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error(f"Failed to resume task: {e}")
|
|
215
|
+
raise HTTPException(status_code=500, detail=f"Failed to resume task: {str(e)}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@router.post("/stop")
|
|
219
|
+
async def stop_task(control_request: TaskControlRequest):
|
|
220
|
+
"""Stop current task execution"""
|
|
221
|
+
from ..shared_state import vibesurf_agent
|
|
222
|
+
|
|
223
|
+
if not vibesurf_agent:
|
|
224
|
+
raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
|
|
225
|
+
|
|
226
|
+
if not is_task_running():
|
|
227
|
+
raise HTTPException(status_code=400, detail="No active task to stop")
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
result = await vibesurf_agent.stop(control_request.reason)
|
|
231
|
+
|
|
232
|
+
if result.success:
|
|
233
|
+
# Update active task status and clear it
|
|
234
|
+
current_task = get_active_task_info()
|
|
235
|
+
if current_task:
|
|
236
|
+
from ..shared_state import active_task
|
|
237
|
+
active_task["status"] = "stopped"
|
|
238
|
+
active_task["stop_reason"] = control_request.reason
|
|
239
|
+
active_task["end_time"] = datetime.now()
|
|
240
|
+
|
|
241
|
+
# Clear active task
|
|
242
|
+
# clear_active_task()
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
"success": True,
|
|
246
|
+
"message": result.message,
|
|
247
|
+
"operation": "stop"
|
|
248
|
+
}
|
|
249
|
+
else:
|
|
250
|
+
raise HTTPException(status_code=500, detail=result.message)
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.error(f"Failed to stop task: {e}")
|
|
254
|
+
raise HTTPException(status_code=500, detail=f"Failed to stop task: {str(e)}")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@router.get("/detailed-status")
|
|
258
|
+
async def get_detailed_task_status():
|
|
259
|
+
"""Get detailed task execution status with vibesurf information"""
|
|
260
|
+
from ..shared_state import vibesurf_agent
|
|
261
|
+
|
|
262
|
+
if not vibesurf_agent:
|
|
263
|
+
raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
current_task = get_active_task_info()
|
|
267
|
+
|
|
268
|
+
if current_task:
|
|
269
|
+
# Get detailed vibesurf status
|
|
270
|
+
vibesurf_status = vibesurf_agent.get_status()
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
"has_active_task": True,
|
|
274
|
+
"task_id": current_task["task_id"],
|
|
275
|
+
"status": current_task["status"],
|
|
276
|
+
"session_id": current_task["session_id"],
|
|
277
|
+
"task": current_task["task"],
|
|
278
|
+
"start_time": current_task["start_time"].isoformat() if current_task.get("start_time") else None,
|
|
279
|
+
"end_time": current_task.get("end_time").isoformat() if current_task.get("end_time") else None,
|
|
280
|
+
"result": current_task.get("result"),
|
|
281
|
+
"error": current_task.get("error"),
|
|
282
|
+
"pause_reason": current_task.get("pause_reason"),
|
|
283
|
+
"stop_reason": current_task.get("stop_reason"),
|
|
284
|
+
"vibesurf_status": {
|
|
285
|
+
"overall_status": vibesurf_status.overall_status,
|
|
286
|
+
"active_step": vibesurf_status.active_step,
|
|
287
|
+
"agent_statuses": {k: v.dict() for k, v in vibesurf_status.agent_statuses.items()},
|
|
288
|
+
"progress": vibesurf_status.progress,
|
|
289
|
+
"last_update": vibesurf_status.last_update.isoformat()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else:
|
|
293
|
+
return {
|
|
294
|
+
"has_active_task": False,
|
|
295
|
+
"message": "No active task"
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.error(f"Failed to get task status: {e}")
|
|
300
|
+
raise HTTPException(status_code=500, detail=f"Failed to get task status: {str(e)}")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Manager for VibeSurf Session Management
|
|
3
|
+
|
|
4
|
+
Handles database connections, session management, and initialization
|
|
5
|
+
with optimized configuration for real-time operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
10
|
+
from sqlalchemy.orm import sessionmaker
|
|
11
|
+
from sqlalchemy.pool import StaticPool
|
|
12
|
+
from .models import Base
|
|
13
|
+
from typing import AsyncGenerator
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class DatabaseManager:
|
|
19
|
+
"""Database connection and session management"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, database_url: str = None):
|
|
22
|
+
"""Initialize database manager
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
database_url: Database connection URL. Defaults to SQLite if not provided.
|
|
26
|
+
"""
|
|
27
|
+
from .. import shared_state
|
|
28
|
+
self.database_url = database_url or os.getenv(
|
|
29
|
+
'VIBESURF_DATABASE_URL',
|
|
30
|
+
f'sqlite+aiosqlite:///{os.path.join(shared_state.workspace_dir, "vibe_surf.db")}'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Configure engine based on database type
|
|
34
|
+
if self.database_url.startswith('sqlite'):
|
|
35
|
+
# SQLite configuration for development
|
|
36
|
+
self.engine = create_async_engine(
|
|
37
|
+
self.database_url,
|
|
38
|
+
poolclass=StaticPool,
|
|
39
|
+
connect_args={
|
|
40
|
+
"check_same_thread": False,
|
|
41
|
+
"timeout": 30
|
|
42
|
+
},
|
|
43
|
+
echo=False # Set to True for SQL debugging
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
# PostgreSQL/MySQL configuration for production
|
|
47
|
+
self.engine = create_async_engine(
|
|
48
|
+
self.database_url,
|
|
49
|
+
pool_size=20,
|
|
50
|
+
max_overflow=30,
|
|
51
|
+
pool_pre_ping=True,
|
|
52
|
+
pool_recycle=3600,
|
|
53
|
+
echo=False
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.async_session_factory = sessionmaker(
|
|
57
|
+
self.engine,
|
|
58
|
+
class_=AsyncSession,
|
|
59
|
+
expire_on_commit=False
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def create_tables(self):
|
|
63
|
+
"""Create all database tables"""
|
|
64
|
+
async with self.engine.begin() as conn:
|
|
65
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
66
|
+
|
|
67
|
+
async def drop_tables(self):
|
|
68
|
+
"""Drop all database tables"""
|
|
69
|
+
async with self.engine.begin() as conn:
|
|
70
|
+
await conn.run_sync(Base.metadata.drop_all)
|
|
71
|
+
|
|
72
|
+
async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
|
|
73
|
+
"""Get async database session"""
|
|
74
|
+
async with self.async_session_factory() as session:
|
|
75
|
+
try:
|
|
76
|
+
yield session
|
|
77
|
+
await session.commit()
|
|
78
|
+
except Exception:
|
|
79
|
+
await session.rollback()
|
|
80
|
+
raise
|
|
81
|
+
finally:
|
|
82
|
+
await session.close()
|
|
83
|
+
|
|
84
|
+
async def close(self):
|
|
85
|
+
"""Close database connections"""
|
|
86
|
+
await self.engine.dispose()
|
|
87
|
+
|
|
88
|
+
# Dependency for FastAPI
|
|
89
|
+
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
|
90
|
+
"""FastAPI dependency for database sessions"""
|
|
91
|
+
from .. import shared_state
|
|
92
|
+
|
|
93
|
+
if not shared_state.db_manager:
|
|
94
|
+
raise RuntimeError("Database manager not initialized. Call initialize_vibesurf_components() first.")
|
|
95
|
+
|
|
96
|
+
async for session in shared_state.db_manager.get_session():
|
|
97
|
+
yield session
|
|
98
|
+
|
|
99
|
+
# Database initialization script
|
|
100
|
+
async def init_database():
|
|
101
|
+
"""Initialize database with tables"""
|
|
102
|
+
from .. import shared_state
|
|
103
|
+
|
|
104
|
+
logger.info("🗄️ Initializing VibeSurf database...")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
if not shared_state.db_manager:
|
|
108
|
+
raise RuntimeError("Database manager not initialized. Call initialize_vibesurf_components() first.")
|
|
109
|
+
|
|
110
|
+
await shared_state.db_manager.create_tables()
|
|
111
|
+
logger.info("✅ Database tables created successfully")
|
|
112
|
+
logger.info("✅ VibeSurf database ready for single-task execution")
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(f"❌ Database initialization failed: {e}")
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
# For standalone execution, initialize a temporary db_manager
|
|
120
|
+
import os
|
|
121
|
+
from .. import shared_state
|
|
122
|
+
|
|
123
|
+
workspace_dir = os.getenv("VIBESURF_WORKSPACE", os.path.join(os.path.dirname(__file__), "../vibesurf_workspace"))
|
|
124
|
+
database_url = os.getenv(
|
|
125
|
+
'VIBESURF_DATABASE_URL',
|
|
126
|
+
f'sqlite+aiosqlite:///{os.path.join(workspace_dir, "vibe_surf.db")}'
|
|
127
|
+
)
|
|
128
|
+
shared_state.db_manager = DatabaseManager(database_url)
|
|
129
|
+
asyncio.run(init_database())
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Models for VibeSurf Backend - With LLM Profile Management
|
|
3
|
+
|
|
4
|
+
SQLAlchemy models for task execution system with LLM profile management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import Column, String, Text, DateTime, Enum, JSON, Boolean, Index, BigInteger
|
|
8
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
9
|
+
from sqlalchemy.sql import func
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
import enum
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
Base = declarative_base()
|
|
15
|
+
|
|
16
|
+
# Enums for type safety
|
|
17
|
+
class TaskStatus(enum.Enum):
|
|
18
|
+
PENDING = "pending"
|
|
19
|
+
RUNNING = "running"
|
|
20
|
+
PAUSED = "paused"
|
|
21
|
+
COMPLETED = "completed"
|
|
22
|
+
FAILED = "failed"
|
|
23
|
+
STOPPED = "stopped"
|
|
24
|
+
|
|
25
|
+
class LLMProfile(Base):
|
|
26
|
+
"""LLM Profile model for managing LLM configurations with encrypted API keys"""
|
|
27
|
+
__tablename__ = 'llm_profiles'
|
|
28
|
+
|
|
29
|
+
# Primary identifier
|
|
30
|
+
profile_id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
|
31
|
+
profile_name = Column(String(100), nullable=False, unique=True) # User-defined unique name
|
|
32
|
+
|
|
33
|
+
# LLM Configuration
|
|
34
|
+
provider = Column(String(50), nullable=False) # openai, anthropic, google, azure_openai, etc.
|
|
35
|
+
model = Column(String(100), nullable=False)
|
|
36
|
+
base_url = Column(String(500), nullable=True)
|
|
37
|
+
encrypted_api_key = Column(Text, nullable=True) # Encrypted API key using MAC address
|
|
38
|
+
|
|
39
|
+
# LLM Parameters (stored as JSON to allow null values)
|
|
40
|
+
temperature = Column(JSON, nullable=True) # Allow float or null
|
|
41
|
+
max_tokens = Column(JSON, nullable=True) # Allow int or null
|
|
42
|
+
top_p = Column(JSON, nullable=True)
|
|
43
|
+
frequency_penalty = Column(JSON, nullable=True)
|
|
44
|
+
seed = Column(JSON, nullable=True)
|
|
45
|
+
|
|
46
|
+
# Provider-specific configuration
|
|
47
|
+
provider_config = Column(JSON, nullable=True)
|
|
48
|
+
|
|
49
|
+
# Profile metadata
|
|
50
|
+
description = Column(Text, nullable=True)
|
|
51
|
+
is_active = Column(Boolean, default=True, nullable=False)
|
|
52
|
+
is_default = Column(Boolean, default=False, nullable=False)
|
|
53
|
+
|
|
54
|
+
# Timestamps
|
|
55
|
+
created_at = Column(DateTime, nullable=False, default=func.now())
|
|
56
|
+
updated_at = Column(DateTime, nullable=False, default=func.now(), onupdate=func.now())
|
|
57
|
+
last_used_at = Column(DateTime, nullable=True)
|
|
58
|
+
|
|
59
|
+
def __repr__(self):
|
|
60
|
+
return f"<LLMProfile(profile_name={self.profile_name}, provider={self.provider}, model={self.model})>"
|
|
61
|
+
|
|
62
|
+
class Task(Base):
|
|
63
|
+
"""Task model with LLM profile reference and workspace directory"""
|
|
64
|
+
__tablename__ = 'tasks'
|
|
65
|
+
|
|
66
|
+
# Primary identifier
|
|
67
|
+
task_id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
|
68
|
+
|
|
69
|
+
# Session tracking
|
|
70
|
+
session_id = Column(String(36), nullable=False)
|
|
71
|
+
|
|
72
|
+
# Task definition
|
|
73
|
+
task_description = Column(Text, nullable=False)
|
|
74
|
+
status = Column(Enum(TaskStatus), nullable=False, default=TaskStatus.PENDING)
|
|
75
|
+
|
|
76
|
+
# LLM Profile reference (instead of storing LLM config directly)
|
|
77
|
+
llm_profile_name = Column(String(100), nullable=False) # Reference to LLMProfile.profile_name
|
|
78
|
+
|
|
79
|
+
# File uploads and workspace
|
|
80
|
+
upload_files_path = Column(String(500), nullable=True) # Path to uploaded files
|
|
81
|
+
workspace_dir = Column(String(500), nullable=True) # Workspace directory for this task
|
|
82
|
+
|
|
83
|
+
# Configuration (JSON strings without API keys)
|
|
84
|
+
mcp_server_config = Column(Text, nullable=True) # MCP server config as JSON string
|
|
85
|
+
|
|
86
|
+
# Results
|
|
87
|
+
task_result = Column(Text, nullable=True) # Final markdown result
|
|
88
|
+
error_message = Column(Text, nullable=True)
|
|
89
|
+
report_path = Column(String(500), nullable=True) # Generated report file path
|
|
90
|
+
|
|
91
|
+
# Timestamps
|
|
92
|
+
created_at = Column(DateTime, nullable=False, default=func.now())
|
|
93
|
+
updated_at = Column(DateTime, nullable=False, default=func.now(), onupdate=func.now())
|
|
94
|
+
started_at = Column(DateTime, nullable=True)
|
|
95
|
+
completed_at = Column(DateTime, nullable=True)
|
|
96
|
+
|
|
97
|
+
# Additional metadata
|
|
98
|
+
task_metadata = Column(JSON, nullable=True) # Additional context
|
|
99
|
+
|
|
100
|
+
def __repr__(self):
|
|
101
|
+
return f"<Task(task_id={self.task_id}, status={self.status.value}, llm_profile={self.llm_profile_name})>"
|
|
102
|
+
|
|
103
|
+
class UploadedFile(Base):
|
|
104
|
+
"""Model for tracking uploaded files"""
|
|
105
|
+
__tablename__ = "uploaded_files"
|
|
106
|
+
|
|
107
|
+
file_id = Column(String(36), primary_key=True) # UUID7 string
|
|
108
|
+
original_filename = Column(String(255), nullable=False, index=True)
|
|
109
|
+
stored_filename = Column(String(255), nullable=False)
|
|
110
|
+
file_path = Column(Text, nullable=False)
|
|
111
|
+
session_id = Column(String(255), nullable=True, index=True)
|
|
112
|
+
file_size = Column(BigInteger, nullable=False)
|
|
113
|
+
mime_type = Column(String(100), nullable=False)
|
|
114
|
+
upload_time = Column(DateTime, default=func.now(), nullable=False, index=True)
|
|
115
|
+
relative_path = Column(Text, nullable=False) # Relative to workspace_dir
|
|
116
|
+
is_deleted = Column(Boolean, default=False, nullable=False, index=True)
|
|
117
|
+
deleted_at = Column(DateTime, nullable=True)
|
|
118
|
+
|
|
119
|
+
def __repr__(self):
|
|
120
|
+
return f"<UploadedFile(file_id={self.file_id}, filename={self.original_filename}, session={self.session_id})>"
|
|
121
|
+
|
|
122
|
+
# Create useful indexes for performance
|
|
123
|
+
Index('idx_llm_profiles_name', LLMProfile.profile_name)
|
|
124
|
+
Index('idx_llm_profiles_active', LLMProfile.is_active)
|
|
125
|
+
Index('idx_llm_profiles_default', LLMProfile.is_default)
|
|
126
|
+
Index('idx_llm_profiles_provider', LLMProfile.provider)
|
|
127
|
+
|
|
128
|
+
Index('idx_tasks_status', Task.status)
|
|
129
|
+
Index('idx_tasks_session', Task.session_id)
|
|
130
|
+
Index('idx_tasks_llm_profile', Task.llm_profile_name)
|
|
131
|
+
Index('idx_tasks_created', Task.created_at)
|
|
132
|
+
|
|
133
|
+
class McpProfile(Base):
|
|
134
|
+
"""MCP Profile model for managing MCP server configurations"""
|
|
135
|
+
__tablename__ = 'mcp_profiles'
|
|
136
|
+
|
|
137
|
+
# Primary identifier
|
|
138
|
+
mcp_id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
|
139
|
+
display_name = Column(String(100), nullable=False, unique=True) # User-friendly name
|
|
140
|
+
mcp_server_name = Column(String(100), nullable=False, unique=True) # Server identifier (e.g., "filesystem", "markitdown")
|
|
141
|
+
|
|
142
|
+
# MCP Server Configuration
|
|
143
|
+
mcp_server_params = Column(JSON, nullable=False) # {"command": "npx", "args": [...]}
|
|
144
|
+
|
|
145
|
+
# Profile metadata
|
|
146
|
+
description = Column(Text, nullable=True)
|
|
147
|
+
is_active = Column(Boolean, default=True, nullable=False)
|
|
148
|
+
|
|
149
|
+
# Timestamps
|
|
150
|
+
created_at = Column(DateTime, nullable=False, default=func.now())
|
|
151
|
+
updated_at = Column(DateTime, nullable=False, default=func.now(), onupdate=func.now())
|
|
152
|
+
last_used_at = Column(DateTime, nullable=True)
|
|
153
|
+
|
|
154
|
+
def __repr__(self):
|
|
155
|
+
return f"<McpProfile(display_name={self.display_name}, server_name={self.mcp_server_name}, active={self.is_active})>"
|
|
156
|
+
|
|
157
|
+
Index('idx_uploaded_files_session_time', UploadedFile.session_id, UploadedFile.upload_time)
|
|
158
|
+
Index('idx_uploaded_files_active', UploadedFile.is_deleted, UploadedFile.upload_time)
|
|
159
|
+
Index('idx_uploaded_files_filename', UploadedFile.original_filename)
|
|
160
|
+
|
|
161
|
+
# MCP Profile indexes
|
|
162
|
+
Index('idx_mcp_profiles_display_name', McpProfile.display_name)
|
|
163
|
+
Index('idx_mcp_profiles_server_name', McpProfile.mcp_server_name)
|
|
164
|
+
Index('idx_mcp_profiles_active', McpProfile.is_active)
|