vibesurf 0.1.16__py3-none-any.whl → 0.1.18__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/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +6 -12
- vibe_surf/agents/report_writer_agent.py +3 -3
- vibe_surf/agents/vibe_surf_agent.py +17 -14
- vibe_surf/agents/views.py +4 -2
- vibe_surf/backend/api/models.py +3 -0
- vibe_surf/backend/api/task.py +3 -1
- vibe_surf/backend/database/manager.py +187 -4
- vibe_surf/backend/database/migrations/v001_initial_schema.sql +118 -0
- vibe_surf/backend/database/migrations/v002_add_agent_mode.sql +6 -0
- vibe_surf/backend/database/models.py +4 -1
- vibe_surf/backend/database/queries.py +4 -2
- vibe_surf/backend/shared_state.py +5 -3
- vibe_surf/chrome_extension/scripts/api-client.js +4 -2
- vibe_surf/chrome_extension/scripts/ui-manager.js +27 -1
- vibe_surf/chrome_extension/sidepanel.html +5 -0
- vibe_surf/chrome_extension/styles/input.css +11 -3
- vibe_surf/llm/openai_compatible.py +1 -2
- {vibesurf-0.1.16.dist-info → vibesurf-0.1.18.dist-info}/METADATA +1 -1
- {vibesurf-0.1.16.dist-info → vibesurf-0.1.18.dist-info}/RECORD +24 -25
- vibe_surf/backend/migrations/__init__.py +0 -16
- vibe_surf/backend/migrations/init_db.py +0 -303
- vibe_surf/backend/migrations/seed_data.py +0 -236
- {vibesurf-0.1.16.dist-info → vibesurf-0.1.18.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.16.dist-info → vibesurf-0.1.18.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.16.dist-info → vibesurf-0.1.18.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.16.dist-info → vibesurf-0.1.18.dist-info}/top_level.txt +0 -0
vibe_surf/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.18'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 18)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -566,12 +566,6 @@ class BrowserUseAgent(Agent):
|
|
|
566
566
|
agent_run_error = 'Agent stopped programmatically'
|
|
567
567
|
break
|
|
568
568
|
|
|
569
|
-
while self.state.paused:
|
|
570
|
-
await asyncio.sleep(0.2) # Small delay to prevent CPU spinning
|
|
571
|
-
if self.state.stopped: # Allow stopping while paused
|
|
572
|
-
agent_run_error = 'Agent stopped programmatically while paused'
|
|
573
|
-
break
|
|
574
|
-
|
|
575
569
|
if on_step_start is not None:
|
|
576
570
|
await on_step_start(self)
|
|
577
571
|
|
|
@@ -820,12 +814,12 @@ class BrowserUseAgent(Agent):
|
|
|
820
814
|
i = global_action_index + local_index
|
|
821
815
|
|
|
822
816
|
# Original sequential execution logic continues here...
|
|
823
|
-
if i > 0:
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
817
|
+
# if i > 0:
|
|
818
|
+
# # ONLY ALLOW TO CALL `done` IF IT IS A SINGLE ACTION
|
|
819
|
+
# if action.model_dump(exclude_unset=True).get('done') is not None:
|
|
820
|
+
# msg = f'Done action is allowed only as a single action - stopped after action {i} / {total_actions}.'
|
|
821
|
+
# self.logger.debug(msg)
|
|
822
|
+
# break
|
|
829
823
|
|
|
830
824
|
# DOM synchronization check - verify element indexes are still valid AFTER first action
|
|
831
825
|
if action.get_index() is not None and i != 0:
|
|
@@ -32,7 +32,7 @@ class ReportTaskResult(BaseModel):
|
|
|
32
32
|
class ReportWriterAgent:
|
|
33
33
|
"""Agent responsible for generating HTML reports using LLM-controlled flow"""
|
|
34
34
|
|
|
35
|
-
def __init__(self, llm: BaseChatModel, workspace_dir: str, step_callback=None,
|
|
35
|
+
def __init__(self, llm: BaseChatModel, workspace_dir: str, step_callback=None, use_thinking: bool = True):
|
|
36
36
|
"""
|
|
37
37
|
Initialize ReportWriterAgent
|
|
38
38
|
|
|
@@ -44,7 +44,7 @@ class ReportWriterAgent:
|
|
|
44
44
|
self.llm = llm
|
|
45
45
|
self.workspace_dir = os.path.abspath(workspace_dir)
|
|
46
46
|
self.step_callback = step_callback
|
|
47
|
-
self.
|
|
47
|
+
self.use_thinking = use_thinking
|
|
48
48
|
|
|
49
49
|
# Initialize file system and tools
|
|
50
50
|
self.file_system = CustomFileSystem(self.workspace_dir)
|
|
@@ -52,7 +52,7 @@ class ReportWriterAgent:
|
|
|
52
52
|
|
|
53
53
|
# Setup action model and agent output
|
|
54
54
|
self.ActionModel = self.tools.registry.create_action_model()
|
|
55
|
-
if self.
|
|
55
|
+
if self.use_thinking:
|
|
56
56
|
self.AgentOutput = CustomAgentOutput.type_with_custom_actions(self.ActionModel)
|
|
57
57
|
else:
|
|
58
58
|
self.AgentOutput = CustomAgentOutput.type_with_custom_actions_no_thinking(self.ActionModel)
|
|
@@ -38,6 +38,7 @@ from vibe_surf.browser.browser_manager import BrowserManager
|
|
|
38
38
|
from vibe_surf.tools.browser_use_tools import BrowserUseTools
|
|
39
39
|
from vibe_surf.tools.vibesurf_tools import VibeSurfTools
|
|
40
40
|
from vibe_surf.tools.file_system import CustomFileSystem
|
|
41
|
+
from vibe_surf.agents.views import VibeSurfAgentSettings
|
|
41
42
|
|
|
42
43
|
from vibe_surf.logger import get_logger
|
|
43
44
|
|
|
@@ -213,11 +214,11 @@ def create_browser_agent_step_callback(state: VibeSurfState, agent_name: str):
|
|
|
213
214
|
step_msg = f"## Step {step_num}\n\n"
|
|
214
215
|
|
|
215
216
|
# Add thinking if present
|
|
216
|
-
if agent_output.thinking:
|
|
217
|
+
if hasattr(agent_output, 'thinking') and agent_output.thinking:
|
|
217
218
|
step_msg += f"**💡 Thinking:**\n{agent_output.thinking}\n\n"
|
|
218
219
|
|
|
219
220
|
# Add evaluation if present
|
|
220
|
-
if agent_output.evaluation_previous_goal:
|
|
221
|
+
if hasattr(agent_output, 'evaluation_previous_goal') and agent_output.evaluation_previous_goal:
|
|
221
222
|
step_msg += f"**👍 Evaluation:**\n{agent_output.evaluation_previous_goal}\n\n"
|
|
222
223
|
|
|
223
224
|
# Add memory if present
|
|
@@ -225,7 +226,7 @@ def create_browser_agent_step_callback(state: VibeSurfState, agent_name: str):
|
|
|
225
226
|
# step_msg += f"**🧠 Memory:** {agent_output.memory}\n\n"
|
|
226
227
|
|
|
227
228
|
# Add next goal if present
|
|
228
|
-
if agent_output.next_goal:
|
|
229
|
+
if hasattr(agent_output, 'next_goal') and agent_output.next_goal:
|
|
229
230
|
step_msg += f"**🎯 Next Goal:**\n{agent_output.next_goal}\n\n"
|
|
230
231
|
|
|
231
232
|
# Add action summary
|
|
@@ -352,7 +353,7 @@ async def _vibesurf_agent_node_impl(state: VibeSurfState) -> VibeSurfState:
|
|
|
352
353
|
# Create action model and agent output using VibeSurfTools
|
|
353
354
|
vibesurf_agent = state.vibesurf_agent
|
|
354
355
|
ActionModel = vibesurf_agent.tools.registry.create_action_model()
|
|
355
|
-
if vibesurf_agent.
|
|
356
|
+
if vibesurf_agent.settings.agent_mode == "thinking":
|
|
356
357
|
AgentOutput = CustomAgentOutput.type_with_custom_actions(ActionModel)
|
|
357
358
|
else:
|
|
358
359
|
AgentOutput = CustomAgentOutput.type_with_custom_actions_no_thinking(ActionModel)
|
|
@@ -632,7 +633,9 @@ async def execute_parallel_browser_tasks(state: VibeSurfState) -> List[BrowserTa
|
|
|
632
633
|
file_system_path=str(bu_agent_workdir),
|
|
633
634
|
register_new_step_callback=step_callback,
|
|
634
635
|
extend_system_message=EXTEND_BU_SYSTEM_PROMPT,
|
|
635
|
-
token_cost_service=state.vibesurf_agent.token_cost_service
|
|
636
|
+
token_cost_service=state.vibesurf_agent.token_cost_service,
|
|
637
|
+
flash_mode=state.vibesurf_agent.settings.agent_mode == "flash",
|
|
638
|
+
use_thinking=state.vibesurf_agent.settings.agent_mode == "thinking"
|
|
636
639
|
)
|
|
637
640
|
agents.append(agent)
|
|
638
641
|
|
|
@@ -783,7 +786,9 @@ async def execute_single_browser_tasks(state: VibeSurfState) -> BrowserTaskResul
|
|
|
783
786
|
file_system_path=str(bu_agent_workdir),
|
|
784
787
|
register_new_step_callback=step_callback,
|
|
785
788
|
extend_system_message=EXTEND_BU_SYSTEM_PROMPT,
|
|
786
|
-
token_cost_service=state.vibesurf_agent.token_cost_service
|
|
789
|
+
token_cost_service=state.vibesurf_agent.token_cost_service,
|
|
790
|
+
flash_mode=state.vibesurf_agent.settings.agent_mode == "flash",
|
|
791
|
+
use_thinking=state.vibesurf_agent.settings.agent_mode == "thinking"
|
|
787
792
|
)
|
|
788
793
|
if state.vibesurf_agent and hasattr(state.vibesurf_agent, '_running_agents'):
|
|
789
794
|
state.vibesurf_agent._running_agents[agent_id] = agent
|
|
@@ -861,7 +866,7 @@ async def _report_task_execution_node_impl(state: VibeSurfState) -> VibeSurfStat
|
|
|
861
866
|
llm=state.vibesurf_agent.llm,
|
|
862
867
|
workspace_dir=str(state.vibesurf_agent.file_system.get_dir()),
|
|
863
868
|
step_callback=step_callback,
|
|
864
|
-
|
|
869
|
+
use_thinking=state.vibesurf_agent.settings.agent_mode == "thinking",
|
|
865
870
|
)
|
|
866
871
|
|
|
867
872
|
# Register report writer agent for control coordination
|
|
@@ -997,19 +1002,17 @@ class VibeSurfAgent:
|
|
|
997
1002
|
browser_manager: BrowserManager,
|
|
998
1003
|
tools: VibeSurfTools,
|
|
999
1004
|
workspace_dir: str = "./workspace",
|
|
1000
|
-
|
|
1001
|
-
calculate_token_cost: bool = True,
|
|
1005
|
+
settings: Optional[VibeSurfAgentSettings] = None
|
|
1002
1006
|
):
|
|
1003
1007
|
"""Initialize VibeSurfAgent with required components"""
|
|
1004
1008
|
self.llm: BaseChatModel = llm
|
|
1005
|
-
self.
|
|
1006
|
-
self.token_cost_service = TokenCost(include_cost=
|
|
1009
|
+
self.settings = settings or VibeSurfAgentSettings()
|
|
1010
|
+
self.token_cost_service = TokenCost(include_cost=self.settings.calculate_cost)
|
|
1007
1011
|
self.token_cost_service.register_llm(llm)
|
|
1008
1012
|
self.browser_manager: BrowserManager = browser_manager
|
|
1009
1013
|
self.tools: VibeSurfTools = tools
|
|
1010
1014
|
self.workspace_dir = workspace_dir
|
|
1011
1015
|
os.makedirs(self.workspace_dir, exist_ok=True)
|
|
1012
|
-
self.thinking_mode = thinking_mode
|
|
1013
1016
|
|
|
1014
1017
|
self.cur_session_id = None
|
|
1015
1018
|
self.file_system: Optional[CustomFileSystem] = None
|
|
@@ -1540,7 +1543,7 @@ Please continue with your assigned work, incorporating this guidance only if it'
|
|
|
1540
1543
|
task: str,
|
|
1541
1544
|
upload_files: Optional[List[str]] = None,
|
|
1542
1545
|
session_id: Optional[str] = None,
|
|
1543
|
-
|
|
1546
|
+
agent_mode: str = "thinking"
|
|
1544
1547
|
) -> str | None:
|
|
1545
1548
|
"""
|
|
1546
1549
|
Main execution method that returns markdown summary with control capabilities
|
|
@@ -1554,7 +1557,7 @@ Please continue with your assigned work, incorporating this guidance only if it'
|
|
|
1554
1557
|
"""
|
|
1555
1558
|
logger.info(f"🚀 Starting VibeSurfAgent execution for task: {task}. Powered by LLM model: {self.llm.model_name}")
|
|
1556
1559
|
try:
|
|
1557
|
-
self.
|
|
1560
|
+
self.settings.agent_mode = agent_mode
|
|
1558
1561
|
session_id = session_id or self.cur_session_id or uuid7str()
|
|
1559
1562
|
if session_id != self.cur_session_id:
|
|
1560
1563
|
# Load session-specific data when switching sessions
|
vibe_surf/agents/views.py
CHANGED
|
@@ -66,11 +66,13 @@ class VibeSurfAgentSettings(BaseModel):
|
|
|
66
66
|
max_history_items: int | None = None
|
|
67
67
|
include_token_cost: bool = False
|
|
68
68
|
|
|
69
|
-
calculate_cost: bool =
|
|
69
|
+
calculate_cost: bool = True
|
|
70
70
|
include_tool_call_examples: bool = False
|
|
71
71
|
llm_timeout: int = 60 # Timeout in seconds for LLM calls
|
|
72
72
|
step_timeout: int = 180 # Timeout in seconds for each step
|
|
73
73
|
|
|
74
|
+
agent_mode: str = "thinking" # thinking, no-thinking, flash
|
|
75
|
+
|
|
74
76
|
|
|
75
77
|
class CustomAgentOutput(BaseModel):
|
|
76
78
|
model_config = ConfigDict(arbitrary_types_allowed=True, extra='forbid')
|
|
@@ -117,7 +119,7 @@ class CustomAgentOutput(BaseModel):
|
|
|
117
119
|
return schema
|
|
118
120
|
|
|
119
121
|
model = create_model(
|
|
120
|
-
'
|
|
122
|
+
'AgentOutputNoThinking',
|
|
121
123
|
__base__=AgentOutputNoThinking,
|
|
122
124
|
action=(
|
|
123
125
|
list[custom_actions], # type: ignore
|
vibe_surf/backend/api/models.py
CHANGED
|
@@ -106,6 +106,7 @@ class TaskCreateRequest(BaseModel):
|
|
|
106
106
|
llm_profile_name: str = Field(description="LLM profile name to use")
|
|
107
107
|
upload_files_path: Optional[str] = Field(default=None, description="Path to uploaded files")
|
|
108
108
|
mcp_server_config: Optional[Dict[str, Any]] = Field(default=None, description="MCP server configuration")
|
|
109
|
+
agent_mode: str = Field(default="thinking", description="Agent mode: 'thinking', 'no-thinking', or 'flash'")
|
|
109
110
|
|
|
110
111
|
class TaskControlRequest(BaseModel):
|
|
111
112
|
"""Request model for task control operations (pause/resume/stop)"""
|
|
@@ -121,6 +122,7 @@ class TaskResponse(BaseModel):
|
|
|
121
122
|
upload_files_path: Optional[str] = None
|
|
122
123
|
workspace_dir: Optional[str] = None
|
|
123
124
|
mcp_server_config: Optional[Dict[str, Any]] = None
|
|
125
|
+
agent_mode: str = "thinking"
|
|
124
126
|
task_result: Optional[str] = None
|
|
125
127
|
error_message: Optional[str] = None
|
|
126
128
|
report_path: Optional[str] = None
|
|
@@ -145,6 +147,7 @@ class TaskResponse(BaseModel):
|
|
|
145
147
|
upload_files_path=task.upload_files_path,
|
|
146
148
|
workspace_dir=task.workspace_dir,
|
|
147
149
|
mcp_server_config=task.mcp_server_config,
|
|
150
|
+
agent_mode=task.agent_mode,
|
|
148
151
|
task_result=task.task_result,
|
|
149
152
|
error_message=task.error_message,
|
|
150
153
|
report_path=task.report_path,
|
vibe_surf/backend/api/task.py
CHANGED
|
@@ -117,7 +117,8 @@ async def submit_task(
|
|
|
117
117
|
mcp_server_config=mcp_server_config,
|
|
118
118
|
llm_profile_name=task_request.llm_profile_name,
|
|
119
119
|
workspace_dir=workspace_dir,
|
|
120
|
-
task_status="pending"
|
|
120
|
+
task_status="pending",
|
|
121
|
+
agent_mode=task_request.agent_mode
|
|
121
122
|
)
|
|
122
123
|
await db.commit()
|
|
123
124
|
|
|
@@ -129,6 +130,7 @@ async def submit_task(
|
|
|
129
130
|
task=task_request.task_description,
|
|
130
131
|
llm_profile_name=task_request.llm_profile_name,
|
|
131
132
|
upload_files=task_request.upload_files_path,
|
|
133
|
+
agent_mode=task_request.agent_mode,
|
|
132
134
|
db_session=db
|
|
133
135
|
)
|
|
134
136
|
|
|
@@ -6,18 +6,145 @@ with optimized configuration for real-time operations.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import glob
|
|
11
|
+
import pdb
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import AsyncGenerator, List, Tuple, Optional
|
|
15
|
+
import logging
|
|
16
|
+
import aiosqlite
|
|
9
17
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
10
18
|
from sqlalchemy.orm import sessionmaker
|
|
11
19
|
from sqlalchemy.pool import StaticPool
|
|
12
20
|
from .models import Base
|
|
13
|
-
from typing import AsyncGenerator
|
|
14
|
-
import logging
|
|
15
21
|
|
|
16
22
|
from vibe_surf.logger import get_logger
|
|
17
23
|
|
|
18
24
|
logger = get_logger(__name__)
|
|
19
25
|
|
|
20
26
|
|
|
27
|
+
class DBMigrationManager:
|
|
28
|
+
"""Simplified database migration manager."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, db_path: str = None):
|
|
31
|
+
"""Initialize migration manager
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
db_path: Database file path. Will be extracted from database_url if not provided.
|
|
35
|
+
"""
|
|
36
|
+
if db_path is None:
|
|
37
|
+
# Extract database path from shared_state
|
|
38
|
+
from .. import shared_state
|
|
39
|
+
database_url = os.getenv(
|
|
40
|
+
'VIBESURF_DATABASE_URL',
|
|
41
|
+
f'sqlite+aiosqlite:///{os.path.join(shared_state.workspace_dir, "vibe_surf.db")}'
|
|
42
|
+
)
|
|
43
|
+
self.db_path = database_url
|
|
44
|
+
else:
|
|
45
|
+
self.db_path = db_path
|
|
46
|
+
|
|
47
|
+
# Extract path from sqlite URL
|
|
48
|
+
if self.db_path.startswith('sqlite+aiosqlite:///'):
|
|
49
|
+
self.db_path = self.db_path[20:] # Remove 'sqlite+aiosqlite:///' prefix
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError(f"Migration manager only supports SQLite databases, got: {self.db_path}")
|
|
52
|
+
|
|
53
|
+
self.migrations_dir = Path(__file__).parent / "migrations"
|
|
54
|
+
|
|
55
|
+
async def get_db_version(self) -> int:
|
|
56
|
+
"""Get current database version."""
|
|
57
|
+
try:
|
|
58
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
59
|
+
cursor = await db.execute("PRAGMA user_version;")
|
|
60
|
+
result = await cursor.fetchone()
|
|
61
|
+
return result[0] if result else 0
|
|
62
|
+
except Exception:
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
async def set_db_version(self, version: int) -> None:
|
|
66
|
+
"""Set database version."""
|
|
67
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
68
|
+
await db.execute(f"PRAGMA user_version = {version};")
|
|
69
|
+
await db.commit()
|
|
70
|
+
|
|
71
|
+
def get_migration_files(self) -> List[Tuple[int, str]]:
|
|
72
|
+
"""Get migration files sorted by version. Returns (version, filepath) tuples."""
|
|
73
|
+
migrations = []
|
|
74
|
+
pattern = str(self.migrations_dir / "v*.sql")
|
|
75
|
+
|
|
76
|
+
for filepath in glob.glob(pattern):
|
|
77
|
+
filename = os.path.basename(filepath)
|
|
78
|
+
match = re.match(r'v(\d+)_.*\.sql$', filename)
|
|
79
|
+
if match:
|
|
80
|
+
version = int(match.group(1))
|
|
81
|
+
migrations.append((version, filepath))
|
|
82
|
+
else:
|
|
83
|
+
logger.warning(f"Migration file {filename} doesn't match pattern v000_description.sql")
|
|
84
|
+
|
|
85
|
+
return sorted(migrations, key=lambda x: x[0])
|
|
86
|
+
|
|
87
|
+
async def init_database(self) -> None:
|
|
88
|
+
"""Initialize database directory if needed."""
|
|
89
|
+
db_dir = os.path.dirname(self.db_path)
|
|
90
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
current_version = await self.get_db_version()
|
|
93
|
+
|
|
94
|
+
if current_version == 0:
|
|
95
|
+
logger.info("Database version is 0, will be initialized via migrations...")
|
|
96
|
+
else:
|
|
97
|
+
logger.info(f"Database already exists (version {current_version})")
|
|
98
|
+
|
|
99
|
+
async def apply_migrations(self, target_version: Optional[int] = None) -> int:
|
|
100
|
+
"""Apply pending migrations. Returns final version."""
|
|
101
|
+
await self.init_database() # Ensure database exists
|
|
102
|
+
|
|
103
|
+
current_version = await self.get_db_version()
|
|
104
|
+
migrations = self.get_migration_files()
|
|
105
|
+
|
|
106
|
+
if not migrations:
|
|
107
|
+
logger.info("No migration files found")
|
|
108
|
+
return current_version
|
|
109
|
+
|
|
110
|
+
if target_version is None:
|
|
111
|
+
target_version = max(m[0] for m in migrations)
|
|
112
|
+
|
|
113
|
+
logger.info(f"Current version: {current_version}, Target: {target_version}")
|
|
114
|
+
|
|
115
|
+
if current_version >= target_version:
|
|
116
|
+
logger.info("Database is up to date")
|
|
117
|
+
return current_version
|
|
118
|
+
|
|
119
|
+
applied = 0
|
|
120
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
121
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
122
|
+
|
|
123
|
+
for version, filepath in migrations:
|
|
124
|
+
if version <= current_version or version > target_version:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
logger.info(f"Applying migration v{version:03d}: {os.path.basename(filepath)}")
|
|
129
|
+
|
|
130
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
131
|
+
sql_content = f.read()
|
|
132
|
+
|
|
133
|
+
await db.executescript(sql_content)
|
|
134
|
+
await self.set_db_version(version)
|
|
135
|
+
applied += 1
|
|
136
|
+
|
|
137
|
+
logger.info(f"Successfully applied migration v{version:03d}")
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Migration v{version:03d} failed: {e}")
|
|
141
|
+
raise RuntimeError(f"Migration failed at version {version}: {e}")
|
|
142
|
+
|
|
143
|
+
final_version = await self.get_db_version()
|
|
144
|
+
logger.info(f"Applied {applied} migrations. Final version: {final_version}")
|
|
145
|
+
return final_version
|
|
146
|
+
|
|
147
|
+
|
|
21
148
|
class DatabaseManager:
|
|
22
149
|
"""Database connection and session management"""
|
|
23
150
|
|
|
@@ -61,11 +188,38 @@ class DatabaseManager:
|
|
|
61
188
|
class_=AsyncSession,
|
|
62
189
|
expire_on_commit=False
|
|
63
190
|
)
|
|
191
|
+
|
|
192
|
+
# Initialize migration manager for SQLite databases
|
|
193
|
+
if self.database_url.startswith('sqlite'):
|
|
194
|
+
try:
|
|
195
|
+
self.migration_manager = DBMigrationManager(db_path=self.database_url)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.warning(f"Failed to initialize migration manager: {e}")
|
|
198
|
+
self.migration_manager = None
|
|
199
|
+
else:
|
|
200
|
+
self.migration_manager = None
|
|
201
|
+
logger.info("Migration manager is only supported for SQLite databases")
|
|
64
202
|
|
|
65
|
-
async def create_tables(self):
|
|
66
|
-
"""Create all database tables
|
|
203
|
+
async def create_tables(self, use_migrations: bool = True):
|
|
204
|
+
"""Create all database tables
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
use_migrations: If True, use migration system. If False, use direct table creation.
|
|
208
|
+
"""
|
|
209
|
+
if use_migrations and self.migration_manager:
|
|
210
|
+
logger.info("🔄 Using migration system to initialize database...")
|
|
211
|
+
try:
|
|
212
|
+
await self.migration_manager.apply_migrations()
|
|
213
|
+
logger.info("✅ Database initialized via migrations")
|
|
214
|
+
return
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.warning(f"Migration failed, falling back to direct table creation: {e}")
|
|
217
|
+
|
|
218
|
+
# Fallback to direct table creation
|
|
219
|
+
logger.info("🔄 Using direct table creation...")
|
|
67
220
|
async with self.engine.begin() as conn:
|
|
68
221
|
await conn.run_sync(Base.metadata.create_all)
|
|
222
|
+
logger.info("✅ Database initialized via direct table creation")
|
|
69
223
|
|
|
70
224
|
async def drop_tables(self):
|
|
71
225
|
"""Drop all database tables"""
|
|
@@ -84,6 +238,35 @@ class DatabaseManager:
|
|
|
84
238
|
finally:
|
|
85
239
|
await session.close()
|
|
86
240
|
|
|
241
|
+
async def apply_migrations(self, target_version: Optional[int] = None) -> int:
|
|
242
|
+
"""Apply database migrations
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
target_version: Target migration version. If None, applies all available migrations.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Final database version after applying migrations.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
RuntimeError: If migration manager is not available or migration fails.
|
|
252
|
+
"""
|
|
253
|
+
if not self.migration_manager:
|
|
254
|
+
raise RuntimeError("Migration manager is not available. Only SQLite databases support migrations.")
|
|
255
|
+
|
|
256
|
+
return await self.migration_manager.apply_migrations(target_version)
|
|
257
|
+
|
|
258
|
+
async def get_db_version(self) -> int:
|
|
259
|
+
"""Get current database version
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Current database version, or 0 if not available.
|
|
263
|
+
"""
|
|
264
|
+
if not self.migration_manager:
|
|
265
|
+
logger.warning("Migration manager not available, cannot get database version")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
return await self.migration_manager.get_db_version()
|
|
269
|
+
|
|
87
270
|
async def close(self):
|
|
88
271
|
"""Close database connections"""
|
|
89
272
|
await self.engine.dispose()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
-- Migration: v001_initial_schema.sql
|
|
2
|
+
-- Description: Initial database schema creation
|
|
3
|
+
-- Version: 0.0.1
|
|
4
|
+
|
|
5
|
+
-- Enable foreign keys
|
|
6
|
+
PRAGMA foreign_keys = ON;
|
|
7
|
+
|
|
8
|
+
-- Create LLM Profiles table
|
|
9
|
+
CREATE TABLE IF NOT EXISTS llm_profiles (
|
|
10
|
+
profile_id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
11
|
+
profile_name VARCHAR(100) NOT NULL UNIQUE,
|
|
12
|
+
provider VARCHAR(50) NOT NULL,
|
|
13
|
+
model VARCHAR(100) NOT NULL,
|
|
14
|
+
base_url VARCHAR(500),
|
|
15
|
+
encrypted_api_key TEXT,
|
|
16
|
+
temperature JSON,
|
|
17
|
+
max_tokens JSON,
|
|
18
|
+
top_p JSON,
|
|
19
|
+
frequency_penalty JSON,
|
|
20
|
+
seed JSON,
|
|
21
|
+
provider_config JSON,
|
|
22
|
+
description TEXT,
|
|
23
|
+
is_active BOOLEAN NOT NULL DEFAULT 1,
|
|
24
|
+
is_default BOOLEAN NOT NULL DEFAULT 0,
|
|
25
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
26
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
27
|
+
last_used_at DATETIME
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- Create Tasks table
|
|
31
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
32
|
+
task_id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
33
|
+
session_id VARCHAR(36) NOT NULL,
|
|
34
|
+
task_description TEXT NOT NULL,
|
|
35
|
+
status VARCHAR(9) NOT NULL DEFAULT 'pending',
|
|
36
|
+
llm_profile_name VARCHAR(100) NOT NULL,
|
|
37
|
+
upload_files_path VARCHAR(500),
|
|
38
|
+
workspace_dir VARCHAR(500),
|
|
39
|
+
mcp_server_config TEXT,
|
|
40
|
+
task_result TEXT,
|
|
41
|
+
error_message TEXT,
|
|
42
|
+
report_path VARCHAR(500),
|
|
43
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
44
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
started_at DATETIME,
|
|
46
|
+
completed_at DATETIME,
|
|
47
|
+
task_metadata JSON,
|
|
48
|
+
CHECK (status IN ('pending', 'running', 'paused', 'completed', 'failed', 'stopped'))
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- Create Uploaded Files table
|
|
52
|
+
CREATE TABLE IF NOT EXISTS uploaded_files (
|
|
53
|
+
file_id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
54
|
+
original_filename VARCHAR(255) NOT NULL,
|
|
55
|
+
stored_filename VARCHAR(255) NOT NULL,
|
|
56
|
+
file_path TEXT NOT NULL,
|
|
57
|
+
session_id VARCHAR(255),
|
|
58
|
+
file_size BIGINT NOT NULL,
|
|
59
|
+
mime_type VARCHAR(100) NOT NULL,
|
|
60
|
+
upload_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
+
relative_path TEXT NOT NULL,
|
|
62
|
+
is_deleted BOOLEAN NOT NULL DEFAULT 0,
|
|
63
|
+
deleted_at DATETIME
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Create MCP Profiles table
|
|
67
|
+
CREATE TABLE IF NOT EXISTS mcp_profiles (
|
|
68
|
+
mcp_id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
69
|
+
display_name VARCHAR(100) NOT NULL UNIQUE,
|
|
70
|
+
mcp_server_name VARCHAR(100) NOT NULL UNIQUE,
|
|
71
|
+
mcp_server_params JSON NOT NULL,
|
|
72
|
+
description TEXT,
|
|
73
|
+
is_active BOOLEAN NOT NULL DEFAULT 1,
|
|
74
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
75
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
76
|
+
last_used_at DATETIME
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
-- Create indexes for performance
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_llm_profiles_name ON llm_profiles(profile_name);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_llm_profiles_active ON llm_profiles(is_active);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_llm_profiles_default ON llm_profiles(is_default);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_llm_profiles_provider ON llm_profiles(provider);
|
|
84
|
+
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_llm_profile ON tasks(llm_profile_name);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_created ON tasks(created_at);
|
|
89
|
+
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_uploaded_files_session_time ON uploaded_files(session_id, upload_time);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_uploaded_files_active ON uploaded_files(is_deleted, upload_time);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_uploaded_files_filename ON uploaded_files(original_filename);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_profiles_display_name ON mcp_profiles(display_name);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_profiles_server_name ON mcp_profiles(mcp_server_name);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_profiles_active ON mcp_profiles(is_active);
|
|
97
|
+
|
|
98
|
+
-- Create triggers for automatic timestamp updates
|
|
99
|
+
CREATE TRIGGER IF NOT EXISTS update_llm_profiles_updated_at
|
|
100
|
+
AFTER UPDATE ON llm_profiles
|
|
101
|
+
FOR EACH ROW
|
|
102
|
+
BEGIN
|
|
103
|
+
UPDATE llm_profiles SET updated_at = CURRENT_TIMESTAMP WHERE profile_id = OLD.profile_id;
|
|
104
|
+
END;
|
|
105
|
+
|
|
106
|
+
CREATE TRIGGER IF NOT EXISTS update_tasks_updated_at
|
|
107
|
+
AFTER UPDATE ON tasks
|
|
108
|
+
FOR EACH ROW
|
|
109
|
+
BEGIN
|
|
110
|
+
UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE task_id = OLD.task_id;
|
|
111
|
+
END;
|
|
112
|
+
|
|
113
|
+
CREATE TRIGGER IF NOT EXISTS update_mcp_profiles_updated_at
|
|
114
|
+
AFTER UPDATE ON mcp_profiles
|
|
115
|
+
FOR EACH ROW
|
|
116
|
+
BEGIN
|
|
117
|
+
UPDATE mcp_profiles SET updated_at = CURRENT_TIMESTAMP WHERE mcp_id = OLD.mcp_id;
|
|
118
|
+
END;
|
|
@@ -71,7 +71,7 @@ class Task(Base):
|
|
|
71
71
|
|
|
72
72
|
# Task definition
|
|
73
73
|
task_description = Column(Text, nullable=False)
|
|
74
|
-
status = Column(Enum(TaskStatus), nullable=False, default=TaskStatus.PENDING)
|
|
74
|
+
status = Column(Enum(TaskStatus, values_callable=lambda obj: [e.value for e in obj]), nullable=False, default=TaskStatus.PENDING)
|
|
75
75
|
|
|
76
76
|
# LLM Profile reference (instead of storing LLM config directly)
|
|
77
77
|
llm_profile_name = Column(String(100), nullable=False) # Reference to LLMProfile.profile_name
|
|
@@ -83,6 +83,9 @@ class Task(Base):
|
|
|
83
83
|
# Configuration (JSON strings without API keys)
|
|
84
84
|
mcp_server_config = Column(Text, nullable=True) # MCP server config as JSON string
|
|
85
85
|
|
|
86
|
+
# Agent execution mode
|
|
87
|
+
agent_mode = Column(String(50), nullable=False, default='thinking') # Agent mode: 'thinking' or 'direct'
|
|
88
|
+
|
|
86
89
|
# Results
|
|
87
90
|
task_result = Column(Text, nullable=True) # Final markdown result
|
|
88
91
|
error_message = Column(Text, nullable=True)
|
|
@@ -454,7 +454,8 @@ class TaskQueries:
|
|
|
454
454
|
task_result: Optional[str] = None,
|
|
455
455
|
task_status: str = "pending",
|
|
456
456
|
error_message: Optional[str] = None,
|
|
457
|
-
report_path: Optional[str] = None
|
|
457
|
+
report_path: Optional[str] = None,
|
|
458
|
+
agent_mode: str = "thinking"
|
|
458
459
|
) -> Task:
|
|
459
460
|
"""Create or update a task record"""
|
|
460
461
|
try:
|
|
@@ -507,7 +508,8 @@ class TaskQueries:
|
|
|
507
508
|
mcp_server_config=mcp_server_config_json,
|
|
508
509
|
task_result=task_result,
|
|
509
510
|
error_message=error_message,
|
|
510
|
-
report_path=report_path
|
|
511
|
+
report_path=report_path,
|
|
512
|
+
agent_mode=agent_mode
|
|
511
513
|
)
|
|
512
514
|
|
|
513
515
|
db.add(task)
|
|
@@ -106,6 +106,7 @@ async def execute_task_background(
|
|
|
106
106
|
task: str,
|
|
107
107
|
llm_profile_name: str,
|
|
108
108
|
upload_files: Optional[List[str]] = None,
|
|
109
|
+
agent_mode: str = "thinking",
|
|
109
110
|
db_session=None
|
|
110
111
|
):
|
|
111
112
|
"""Background task execution function for single task with LLM profile support"""
|
|
@@ -141,7 +142,8 @@ async def execute_task_background(
|
|
|
141
142
|
result = await vibesurf_agent.run(
|
|
142
143
|
task=task,
|
|
143
144
|
upload_files=upload_files,
|
|
144
|
-
session_id=session_id
|
|
145
|
+
session_id=session_id,
|
|
146
|
+
agent_mode=agent_mode
|
|
145
147
|
)
|
|
146
148
|
|
|
147
149
|
# Update task status to completed
|
|
@@ -400,8 +402,8 @@ async def initialize_vibesurf_components():
|
|
|
400
402
|
|
|
401
403
|
db_manager = DatabaseManager(database_url)
|
|
402
404
|
|
|
403
|
-
# Initialize database tables
|
|
404
|
-
await db_manager.create_tables()
|
|
405
|
+
# Initialize database tables with migration support
|
|
406
|
+
await db_manager.create_tables(use_migrations=True)
|
|
405
407
|
logger.info("✅ Database manager initialized successfully")
|
|
406
408
|
|
|
407
409
|
# Initialize LLM from default profile (if available) or fallback to environment variables
|