vibesurf 0.1.10__py3-none-any.whl → 0.1.11__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 +68 -45
- vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
- vibe_surf/agents/report_writer_agent.py +380 -226
- vibe_surf/agents/vibe_surf_agent.py +879 -825
- vibe_surf/agents/views.py +130 -0
- vibe_surf/backend/api/activity.py +3 -1
- vibe_surf/backend/api/browser.py +9 -5
- vibe_surf/backend/api/config.py +8 -5
- vibe_surf/backend/api/files.py +59 -50
- vibe_surf/backend/api/models.py +2 -2
- vibe_surf/backend/api/task.py +45 -12
- vibe_surf/backend/database/manager.py +24 -18
- vibe_surf/backend/database/queries.py +199 -192
- vibe_surf/backend/database/schemas.py +1 -1
- vibe_surf/backend/main.py +4 -2
- vibe_surf/backend/shared_state.py +28 -35
- vibe_surf/backend/utils/encryption.py +3 -1
- vibe_surf/backend/utils/llm_factory.py +41 -36
- vibe_surf/browser/agent_browser_session.py +0 -4
- vibe_surf/browser/browser_manager.py +14 -8
- vibe_surf/browser/utils.py +5 -3
- vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
- vibe_surf/chrome_extension/background.js +4 -0
- vibe_surf/chrome_extension/scripts/api-client.js +13 -0
- vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
- vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
- vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
- vibe_surf/chrome_extension/sidepanel.html +21 -4
- vibe_surf/chrome_extension/styles/activity.css +365 -5
- vibe_surf/chrome_extension/styles/input.css +139 -0
- vibe_surf/cli.py +4 -22
- vibe_surf/common.py +35 -0
- vibe_surf/llm/openai_compatible.py +148 -93
- vibe_surf/logger.py +99 -0
- vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
- vibe_surf/tools/file_system.py +415 -0
- vibe_surf/{controller → tools}/mcp_client.py +4 -3
- vibe_surf/tools/report_writer_tools.py +21 -0
- vibe_surf/tools/vibesurf_tools.py +657 -0
- vibe_surf/tools/views.py +120 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/METADATA +6 -2
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/RECORD +49 -43
- vibe_surf/controller/file_system.py +0 -53
- vibe_surf/controller/views.py +0 -37
- /vibe_surf/{controller → tools}/__init__.py +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pickle
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
from uuid_extensions import uuid7str
|
|
12
|
+
from json_repair import repair_json
|
|
13
|
+
|
|
14
|
+
from browser_use.browser.session import BrowserSession
|
|
15
|
+
from browser_use.llm.base import BaseChatModel
|
|
16
|
+
from browser_use.llm.messages import UserMessage, SystemMessage, BaseMessage, AssistantMessage, ContentPartTextParam, \
|
|
17
|
+
ContentPartImageParam, ImageURL
|
|
18
|
+
from browser_use.browser.views import TabInfo, BrowserStateSummary
|
|
19
|
+
from browser_use.filesystem.file_system import FileSystem
|
|
20
|
+
from browser_use.agent.views import AgentSettings
|
|
21
|
+
from pydantic import BaseModel, Field, ConfigDict, create_model
|
|
22
|
+
from browser_use.agent.views import AgentSettings, DEFAULT_INCLUDE_ATTRIBUTES
|
|
23
|
+
from browser_use.tools.registry.views import ActionModel
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VibeSurfAgentOutput(BaseModel):
|
|
27
|
+
"""Agent output model following browser_use patterns"""
|
|
28
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra='forbid')
|
|
29
|
+
|
|
30
|
+
thinking: str | None = None
|
|
31
|
+
action: List[Any] = Field(
|
|
32
|
+
...,
|
|
33
|
+
description='List of actions to execute',
|
|
34
|
+
json_schema_extra={'min_items': 1},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def model_json_schema(cls, **kwargs):
|
|
39
|
+
schema = super().model_json_schema(**kwargs)
|
|
40
|
+
schema['required'] = ['thinking', 'action']
|
|
41
|
+
return schema
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def type_with_custom_actions(custom_actions: type) -> type:
|
|
45
|
+
"""Extend actions with custom actions"""
|
|
46
|
+
model_ = create_model(
|
|
47
|
+
'VibeSurfAgentOutput',
|
|
48
|
+
__base__=VibeSurfAgentOutput,
|
|
49
|
+
action=(
|
|
50
|
+
list[custom_actions], # type: ignore
|
|
51
|
+
Field(..., description='List of actions to execute', json_schema_extra={'min_items': 1}),
|
|
52
|
+
),
|
|
53
|
+
__module__=VibeSurfAgentOutput.__module__,
|
|
54
|
+
)
|
|
55
|
+
model_.__doc__ = 'VibeSurfAgentOutput model with custom actions'
|
|
56
|
+
return model_
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class VibeSurfAgentSettings(BaseModel):
|
|
60
|
+
use_vision: bool = True
|
|
61
|
+
max_failures: int = 3
|
|
62
|
+
override_system_message: str | None = None
|
|
63
|
+
extend_system_message: str | None = None
|
|
64
|
+
include_attributes: list[str] | None = DEFAULT_INCLUDE_ATTRIBUTES
|
|
65
|
+
max_actions_per_step: int = 4
|
|
66
|
+
max_history_items: int | None = None
|
|
67
|
+
include_token_cost: bool = False
|
|
68
|
+
|
|
69
|
+
calculate_cost: bool = False
|
|
70
|
+
include_tool_call_examples: bool = False
|
|
71
|
+
llm_timeout: int = 60 # Timeout in seconds for LLM calls
|
|
72
|
+
step_timeout: int = 180 # Timeout in seconds for each step
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CustomAgentOutput(BaseModel):
|
|
76
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra='forbid')
|
|
77
|
+
|
|
78
|
+
thinking: str | None = None
|
|
79
|
+
action: list[ActionModel] = Field(
|
|
80
|
+
...,
|
|
81
|
+
description='List of actions to execute',
|
|
82
|
+
json_schema_extra={'min_items': 1}, # Ensure at least one action is provided
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def model_json_schema(cls, **kwargs):
|
|
87
|
+
schema = super().model_json_schema(**kwargs)
|
|
88
|
+
schema['required'] = ['action']
|
|
89
|
+
return schema
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def type_with_custom_actions(custom_actions: type[ActionModel]) -> type['CustomAgentOutput']:
|
|
93
|
+
"""Extend actions with custom actions"""
|
|
94
|
+
|
|
95
|
+
model_ = create_model(
|
|
96
|
+
'AgentOutput',
|
|
97
|
+
__base__=CustomAgentOutput,
|
|
98
|
+
action=(
|
|
99
|
+
list[custom_actions], # type: ignore
|
|
100
|
+
Field(..., description='List of actions to execute', json_schema_extra={'min_items': 1}),
|
|
101
|
+
),
|
|
102
|
+
__module__=CustomAgentOutput.__module__,
|
|
103
|
+
)
|
|
104
|
+
model_.__doc__ = 'AgentOutput model with custom actions'
|
|
105
|
+
return model_
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def type_with_custom_actions_no_thinking(custom_actions: type[ActionModel]) -> type['CustomAgentOutput']:
|
|
109
|
+
"""Extend actions with custom actions and exclude thinking field"""
|
|
110
|
+
|
|
111
|
+
class AgentOutputNoThinking(CustomAgentOutput):
|
|
112
|
+
@classmethod
|
|
113
|
+
def model_json_schema(cls, **kwargs):
|
|
114
|
+
schema = super().model_json_schema(**kwargs)
|
|
115
|
+
del schema['properties']['thinking']
|
|
116
|
+
schema['required'] = ['action']
|
|
117
|
+
return schema
|
|
118
|
+
|
|
119
|
+
model = create_model(
|
|
120
|
+
'AgentOutput',
|
|
121
|
+
__base__=AgentOutputNoThinking,
|
|
122
|
+
action=(
|
|
123
|
+
list[custom_actions], # type: ignore
|
|
124
|
+
Field(..., description='List of actions to execute', json_schema_extra={'min_items': 1}),
|
|
125
|
+
),
|
|
126
|
+
__module__=AgentOutputNoThinking.__module__,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
model.__doc__ = 'AgentOutput model with custom actions'
|
|
130
|
+
return model
|
|
@@ -14,7 +14,9 @@ from ..database import get_db_session
|
|
|
14
14
|
from ..database.queries import TaskQueries
|
|
15
15
|
from .models import ActivityQueryRequest, SessionActivityQueryRequest
|
|
16
16
|
|
|
17
|
-
logger
|
|
17
|
+
from vibe_surf.logger import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
18
20
|
|
|
19
21
|
router = APIRouter(prefix="/activity", tags=["activity"])
|
|
20
22
|
|
vibe_surf/backend/api/browser.py
CHANGED
|
@@ -8,16 +8,16 @@ from fastapi import APIRouter, HTTPException
|
|
|
8
8
|
from typing import Dict, Any, Optional
|
|
9
9
|
import logging
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
from ..shared_state import browser_manager
|
|
11
|
+
from vibe_surf.logger import get_logger
|
|
13
12
|
|
|
14
|
-
logger =
|
|
13
|
+
logger = get_logger(__name__)
|
|
15
14
|
|
|
16
15
|
router = APIRouter(prefix="/browser", tags=["browser"])
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
@router.get("/active-tab")
|
|
20
19
|
async def get_active_tab() -> Dict[str, Dict[str, str]]:
|
|
20
|
+
from ..shared_state import browser_manager
|
|
21
21
|
"""Get the current active tab information"""
|
|
22
22
|
if not browser_manager:
|
|
23
23
|
raise HTTPException(status_code=503, detail="Browser manager not initialized")
|
|
@@ -27,11 +27,13 @@ async def get_active_tab() -> Dict[str, Dict[str, str]]:
|
|
|
27
27
|
active_tab_info = await browser_manager.get_activate_tab()
|
|
28
28
|
|
|
29
29
|
if not active_tab_info:
|
|
30
|
+
logger.info("No active tab found!")
|
|
30
31
|
return {}
|
|
31
32
|
|
|
33
|
+
logger.info(active_tab_info)
|
|
32
34
|
# Return dict format: {tab_id: {url: , title: }}
|
|
33
35
|
return {
|
|
34
|
-
active_tab_info.target_id: {
|
|
36
|
+
active_tab_info.target_id[:-4]: {
|
|
35
37
|
"url": active_tab_info.url,
|
|
36
38
|
"title": active_tab_info.title
|
|
37
39
|
}
|
|
@@ -45,6 +47,8 @@ async def get_active_tab() -> Dict[str, Dict[str, str]]:
|
|
|
45
47
|
@router.get("/all-tabs")
|
|
46
48
|
async def get_all_tabs() -> Dict[str, Dict[str, str]]:
|
|
47
49
|
"""Get all browser tabs information"""
|
|
50
|
+
from ..shared_state import browser_manager
|
|
51
|
+
|
|
48
52
|
if not browser_manager:
|
|
49
53
|
raise HTTPException(status_code=503, detail="Browser manager not initialized")
|
|
50
54
|
|
|
@@ -54,7 +58,7 @@ async def get_all_tabs() -> Dict[str, Dict[str, str]]:
|
|
|
54
58
|
# Filter only page targets and build result dict
|
|
55
59
|
result = {}
|
|
56
60
|
for tab_info in all_tab_infos:
|
|
57
|
-
result[tab_info.target_id] = {
|
|
61
|
+
result[tab_info.target_id[-4:]] = {
|
|
58
62
|
"url": tab_info.url,
|
|
59
63
|
"title": tab_info.title
|
|
60
64
|
}
|
vibe_surf/backend/api/config.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Configuration API endpoints for VibeSurf Backend
|
|
3
3
|
|
|
4
|
-
Handles LLM Profile and
|
|
4
|
+
Handles LLM Profile and tools configuration management.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from fastapi import APIRouter, HTTPException, Depends
|
|
@@ -18,7 +18,10 @@ from .models import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
router = APIRouter(prefix="/config", tags=["config"])
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
from vibe_surf.logger import get_logger
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
22
25
|
|
|
23
26
|
def _profile_to_response_dict(profile) -> dict:
|
|
24
27
|
"""Convert SQLAlchemy LLMProfile to dict for Pydantic validation - safe extraction"""
|
|
@@ -654,8 +657,8 @@ async def get_configuration_status(db: AsyncSession = Depends(get_db_session)):
|
|
|
654
657
|
"default_profile": default_profile.profile_name if default_profile else None,
|
|
655
658
|
"has_default": default_profile is not None
|
|
656
659
|
},
|
|
657
|
-
"
|
|
658
|
-
"initialized": shared_state.
|
|
660
|
+
"tools": {
|
|
661
|
+
"initialized": shared_state.vibesurf_tools is not None
|
|
659
662
|
},
|
|
660
663
|
"browser_manager": {
|
|
661
664
|
"initialized": shared_state.browser_manager is not None
|
|
@@ -666,7 +669,7 @@ async def get_configuration_status(db: AsyncSession = Depends(get_db_session)):
|
|
|
666
669
|
},
|
|
667
670
|
"overall_status": "ready" if (
|
|
668
671
|
default_profile and
|
|
669
|
-
shared_state.
|
|
672
|
+
shared_state.vibesurf_tools and
|
|
670
673
|
shared_state.browser_manager and
|
|
671
674
|
shared_state.vibesurf_agent
|
|
672
675
|
) else "partial"
|
vibe_surf/backend/api/files.py
CHANGED
|
@@ -21,39 +21,44 @@ from ..database import get_db_session
|
|
|
21
21
|
from ..database.queries import UploadedFileQueries
|
|
22
22
|
from .models import FileListQueryRequest, SessionFilesQueryRequest
|
|
23
23
|
|
|
24
|
-
logger
|
|
24
|
+
from vibe_surf.logger import get_logger
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
25
27
|
|
|
26
28
|
router = APIRouter(prefix="/files", tags=["files"])
|
|
27
29
|
|
|
30
|
+
|
|
28
31
|
def get_upload_directory(session_id: Optional[str] = None) -> str:
|
|
29
32
|
from ..shared_state import workspace_dir
|
|
30
33
|
"""Get the upload directory path for a session or global uploads"""
|
|
31
34
|
if session_id:
|
|
32
|
-
upload_dir = os.path.join(workspace_dir, session_id, "upload_files")
|
|
35
|
+
upload_dir = os.path.join(workspace_dir, "sessions", session_id, "upload_files")
|
|
33
36
|
else:
|
|
34
|
-
upload_dir = os.path.join(workspace_dir, "upload_files")
|
|
35
|
-
|
|
37
|
+
upload_dir = os.path.join(workspace_dir, "sessions", "upload_files")
|
|
38
|
+
|
|
36
39
|
# Create directory if it doesn't exist
|
|
37
40
|
os.makedirs(upload_dir, exist_ok=True)
|
|
38
41
|
return upload_dir
|
|
39
42
|
|
|
43
|
+
|
|
40
44
|
def is_safe_path(basedir: str, path: str) -> bool:
|
|
41
45
|
"""Check if the path is safe (within basedir)"""
|
|
42
46
|
try:
|
|
43
47
|
# Resolve both paths to absolute paths
|
|
44
48
|
basedir = os.path.abspath(basedir)
|
|
45
49
|
path = os.path.abspath(path)
|
|
46
|
-
|
|
50
|
+
|
|
47
51
|
# Check if path starts with basedir
|
|
48
52
|
return path.startswith(basedir)
|
|
49
53
|
except:
|
|
50
54
|
return False
|
|
51
55
|
|
|
56
|
+
|
|
52
57
|
@router.post("/upload")
|
|
53
58
|
async def upload_files(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
files: List[UploadFile] = File(...),
|
|
60
|
+
session_id: Optional[str] = Form(None),
|
|
61
|
+
db: AsyncSession = Depends(get_db_session)
|
|
57
62
|
):
|
|
58
63
|
"""Upload files to workspace/upload_files folder or session-specific folder"""
|
|
59
64
|
try:
|
|
@@ -61,18 +66,18 @@ async def upload_files(
|
|
|
61
66
|
|
|
62
67
|
upload_dir = get_upload_directory(session_id)
|
|
63
68
|
uploaded_file_info = []
|
|
64
|
-
|
|
69
|
+
|
|
65
70
|
for file in files:
|
|
66
71
|
if not file.filename:
|
|
67
72
|
continue
|
|
68
|
-
|
|
73
|
+
|
|
69
74
|
# Generate unique file ID
|
|
70
75
|
file_id = uuid7str()
|
|
71
|
-
|
|
76
|
+
|
|
72
77
|
# Create safe filename
|
|
73
78
|
filename = file.filename
|
|
74
79
|
file_path = os.path.join(upload_dir, filename)
|
|
75
|
-
|
|
80
|
+
|
|
76
81
|
# Handle duplicate filenames by adding suffix
|
|
77
82
|
counter = 1
|
|
78
83
|
base_name, ext = os.path.splitext(filename)
|
|
@@ -81,21 +86,21 @@ async def upload_files(
|
|
|
81
86
|
file_path = os.path.join(upload_dir, new_filename)
|
|
82
87
|
filename = new_filename
|
|
83
88
|
counter += 1
|
|
84
|
-
|
|
89
|
+
|
|
85
90
|
# Ensure path is safe
|
|
86
91
|
if not is_safe_path(upload_dir, file_path):
|
|
87
92
|
raise HTTPException(status_code=400, detail=f"Invalid file path: {filename}")
|
|
88
|
-
|
|
93
|
+
|
|
89
94
|
# Save file
|
|
90
95
|
try:
|
|
91
96
|
with open(file_path, "wb") as buffer:
|
|
92
97
|
shutil.copyfileobj(file.file, buffer)
|
|
93
|
-
|
|
98
|
+
|
|
94
99
|
# Get file info
|
|
95
100
|
file_size = os.path.getsize(file_path)
|
|
96
101
|
mime_type, _ = mimetypes.guess_type(file_path)
|
|
97
102
|
relative_path = os.path.relpath(file_path, workspace_dir)
|
|
98
|
-
|
|
103
|
+
|
|
99
104
|
# Store file metadata in database
|
|
100
105
|
uploaded_file = await UploadedFileQueries.create_file_record(
|
|
101
106
|
db=db,
|
|
@@ -108,7 +113,7 @@ async def upload_files(
|
|
|
108
113
|
mime_type=mime_type or "application/octet-stream",
|
|
109
114
|
relative_path=relative_path
|
|
110
115
|
)
|
|
111
|
-
|
|
116
|
+
|
|
112
117
|
# Create response metadata
|
|
113
118
|
file_metadata = {
|
|
114
119
|
"file_id": uploaded_file.file_id,
|
|
@@ -120,11 +125,11 @@ async def upload_files(
|
|
|
120
125
|
"upload_time": uploaded_file.upload_time.isoformat(),
|
|
121
126
|
"file_path": file_path
|
|
122
127
|
}
|
|
123
|
-
|
|
128
|
+
|
|
124
129
|
uploaded_file_info.append(file_metadata)
|
|
125
|
-
|
|
130
|
+
|
|
126
131
|
logger.info(f"File uploaded: {filename} (ID: {file_id}) to {upload_dir}")
|
|
127
|
-
|
|
132
|
+
|
|
128
133
|
except Exception as e:
|
|
129
134
|
logger.error(f"Failed to save file {filename}: {e}")
|
|
130
135
|
# If database record was created but file save failed, clean up
|
|
@@ -133,20 +138,21 @@ async def upload_files(
|
|
|
133
138
|
except:
|
|
134
139
|
pass
|
|
135
140
|
raise HTTPException(status_code=500, detail=f"Failed to save file {filename}: {str(e)}")
|
|
136
|
-
|
|
141
|
+
|
|
137
142
|
# Commit all database changes
|
|
138
143
|
await db.commit()
|
|
139
|
-
|
|
144
|
+
|
|
140
145
|
return {
|
|
141
146
|
"message": f"Successfully uploaded {len(uploaded_file_info)} files",
|
|
142
147
|
"files": uploaded_file_info,
|
|
143
148
|
"upload_directory": upload_dir
|
|
144
149
|
}
|
|
145
|
-
|
|
150
|
+
|
|
146
151
|
except Exception as e:
|
|
147
152
|
logger.error(f"File upload failed: {e}")
|
|
148
153
|
raise HTTPException(status_code=500, detail=f"File upload failed: {str(e)}")
|
|
149
154
|
|
|
155
|
+
|
|
150
156
|
@router.get("/{file_id}")
|
|
151
157
|
async def download_file(file_id: str, db: AsyncSession = Depends(get_db_session)):
|
|
152
158
|
"""Download file by file ID"""
|
|
@@ -155,26 +161,27 @@ async def download_file(file_id: str, db: AsyncSession = Depends(get_db_session)
|
|
|
155
161
|
uploaded_file = await UploadedFileQueries.get_file(db, file_id)
|
|
156
162
|
if not uploaded_file:
|
|
157
163
|
raise HTTPException(status_code=404, detail="File not found")
|
|
158
|
-
|
|
164
|
+
|
|
159
165
|
file_path = uploaded_file.file_path
|
|
160
|
-
|
|
166
|
+
|
|
161
167
|
if not os.path.exists(file_path):
|
|
162
168
|
raise HTTPException(status_code=404, detail="File not found on disk")
|
|
163
|
-
|
|
169
|
+
|
|
164
170
|
# Ensure path is safe
|
|
165
171
|
if not is_safe_path(workspace_dir, file_path):
|
|
166
172
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
167
|
-
|
|
173
|
+
|
|
168
174
|
return FileResponse(
|
|
169
175
|
path=file_path,
|
|
170
176
|
filename=uploaded_file.original_filename,
|
|
171
177
|
media_type=uploaded_file.mime_type
|
|
172
178
|
)
|
|
173
179
|
|
|
180
|
+
|
|
174
181
|
@router.get("")
|
|
175
182
|
async def list_uploaded_files(
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
query: FileListQueryRequest = Depends(),
|
|
184
|
+
db: AsyncSession = Depends(get_db_session)
|
|
178
185
|
):
|
|
179
186
|
"""List uploaded files, optionally filtered by session"""
|
|
180
187
|
try:
|
|
@@ -186,14 +193,14 @@ async def list_uploaded_files(
|
|
|
186
193
|
offset=query.offset,
|
|
187
194
|
active_only=True
|
|
188
195
|
)
|
|
189
|
-
|
|
196
|
+
|
|
190
197
|
# Get total count
|
|
191
198
|
total_count = await UploadedFileQueries.count_files(
|
|
192
199
|
db=db,
|
|
193
200
|
session_id=query.session_id,
|
|
194
201
|
active_only=True
|
|
195
202
|
)
|
|
196
|
-
|
|
203
|
+
|
|
197
204
|
# Convert to response format (exclude file_path for security)
|
|
198
205
|
files_response = []
|
|
199
206
|
for file_record in uploaded_files:
|
|
@@ -207,7 +214,7 @@ async def list_uploaded_files(
|
|
|
207
214
|
"upload_time": file_record.upload_time.isoformat(),
|
|
208
215
|
"file_path": file_record.file_path
|
|
209
216
|
})
|
|
210
|
-
|
|
217
|
+
|
|
211
218
|
return {
|
|
212
219
|
"files": files_response,
|
|
213
220
|
"total_count": total_count,
|
|
@@ -216,11 +223,12 @@ async def list_uploaded_files(
|
|
|
216
223
|
"has_more": query.limit != -1 and (query.offset + query.limit < total_count),
|
|
217
224
|
"session_id": query.session_id
|
|
218
225
|
}
|
|
219
|
-
|
|
226
|
+
|
|
220
227
|
except Exception as e:
|
|
221
228
|
logger.error(f"Failed to list files: {e}")
|
|
222
229
|
raise HTTPException(status_code=500, detail=f"Failed to list files: {str(e)}")
|
|
223
230
|
|
|
231
|
+
|
|
224
232
|
@router.delete("/{file_id}")
|
|
225
233
|
async def delete_file(file_id: str, db: AsyncSession = Depends(get_db_session)):
|
|
226
234
|
"""Delete uploaded file by file ID"""
|
|
@@ -228,40 +236,41 @@ async def delete_file(file_id: str, db: AsyncSession = Depends(get_db_session)):
|
|
|
228
236
|
uploaded_file = await UploadedFileQueries.get_file(db, file_id)
|
|
229
237
|
if not uploaded_file:
|
|
230
238
|
raise HTTPException(status_code=404, detail="File not found")
|
|
231
|
-
|
|
239
|
+
|
|
232
240
|
try:
|
|
233
241
|
# Remove file from disk
|
|
234
242
|
if os.path.exists(uploaded_file.file_path):
|
|
235
243
|
os.remove(uploaded_file.file_path)
|
|
236
|
-
|
|
244
|
+
|
|
237
245
|
# Soft delete from database
|
|
238
246
|
success = await UploadedFileQueries.delete_file(db, file_id)
|
|
239
247
|
if not success:
|
|
240
248
|
raise HTTPException(status_code=500, detail="Failed to delete file record")
|
|
241
|
-
|
|
249
|
+
|
|
242
250
|
await db.commit()
|
|
243
|
-
|
|
251
|
+
|
|
244
252
|
return {
|
|
245
253
|
"message": f"File {uploaded_file.original_filename} deleted successfully",
|
|
246
254
|
"file_id": file_id
|
|
247
255
|
}
|
|
248
|
-
|
|
256
|
+
|
|
249
257
|
except HTTPException:
|
|
250
258
|
raise
|
|
251
259
|
except Exception as e:
|
|
252
260
|
logger.error(f"Failed to delete file {file_id}: {e}")
|
|
253
261
|
raise HTTPException(status_code=500, detail=f"Failed to delete file: {str(e)}")
|
|
254
262
|
|
|
263
|
+
|
|
255
264
|
@router.get("/session/{session_id}")
|
|
256
265
|
async def list_session_files(
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
session_id: str,
|
|
267
|
+
query: SessionFilesQueryRequest = Depends()
|
|
259
268
|
):
|
|
260
269
|
"""List all files in a session directory"""
|
|
261
270
|
try:
|
|
262
271
|
from ..shared_state import workspace_dir
|
|
263
272
|
session_dir = os.path.join(workspace_dir, session_id)
|
|
264
|
-
|
|
273
|
+
|
|
265
274
|
if not os.path.exists(session_dir):
|
|
266
275
|
return {
|
|
267
276
|
"session_id": session_id,
|
|
@@ -269,16 +278,16 @@ async def list_session_files(
|
|
|
269
278
|
"directories": [],
|
|
270
279
|
"message": "Session directory not found"
|
|
271
280
|
}
|
|
272
|
-
|
|
281
|
+
|
|
273
282
|
files = []
|
|
274
283
|
directories = []
|
|
275
|
-
|
|
284
|
+
|
|
276
285
|
for root, dirs, filenames in os.walk(session_dir):
|
|
277
286
|
# Calculate relative path from session directory
|
|
278
287
|
rel_root = os.path.relpath(root, session_dir)
|
|
279
288
|
if rel_root == ".":
|
|
280
289
|
rel_root = ""
|
|
281
|
-
|
|
290
|
+
|
|
282
291
|
# Add directories if requested
|
|
283
292
|
if query.include_directories:
|
|
284
293
|
for dirname in dirs:
|
|
@@ -288,16 +297,16 @@ async def list_session_files(
|
|
|
288
297
|
"path": dir_path,
|
|
289
298
|
"type": "directory"
|
|
290
299
|
})
|
|
291
|
-
|
|
300
|
+
|
|
292
301
|
# Add files
|
|
293
302
|
for filename in filenames:
|
|
294
303
|
file_path = os.path.join(root, filename)
|
|
295
304
|
rel_path = os.path.join(rel_root, filename) if rel_root else filename
|
|
296
|
-
|
|
305
|
+
|
|
297
306
|
try:
|
|
298
307
|
stat = os.stat(file_path)
|
|
299
308
|
mime_type, _ = mimetypes.guess_type(file_path)
|
|
300
|
-
|
|
309
|
+
|
|
301
310
|
files.append({
|
|
302
311
|
"name": filename,
|
|
303
312
|
"path": rel_path,
|
|
@@ -308,7 +317,7 @@ async def list_session_files(
|
|
|
308
317
|
})
|
|
309
318
|
except Exception as e:
|
|
310
319
|
logger.warning(f"Could not get stats for file {file_path}: {e}")
|
|
311
|
-
|
|
320
|
+
|
|
312
321
|
return {
|
|
313
322
|
"session_id": session_id,
|
|
314
323
|
"files": files,
|
|
@@ -316,7 +325,7 @@ async def list_session_files(
|
|
|
316
325
|
"total_files": len(files),
|
|
317
326
|
"total_directories": len(directories) if query.include_directories else 0
|
|
318
327
|
}
|
|
319
|
-
|
|
328
|
+
|
|
320
329
|
except Exception as e:
|
|
321
330
|
logger.error(f"Failed to list session files for {session_id}: {e}")
|
|
322
|
-
raise HTTPException(status_code=500, detail=f"Failed to list session files: {str(e)}")
|
|
331
|
+
raise HTTPException(status_code=500, detail=f"Failed to list session files: {str(e)}")
|
vibe_surf/backend/api/models.py
CHANGED
|
@@ -245,13 +245,13 @@ class LLMConfigResponse(BaseModel):
|
|
|
245
245
|
available_providers: List[str] = []
|
|
246
246
|
|
|
247
247
|
class ControllerConfigRequest(BaseModel):
|
|
248
|
-
"""Request model for updating
|
|
248
|
+
"""Request model for updating tools configuration"""
|
|
249
249
|
exclude_actions: Optional[List[str]] = Field(default=None, description="Actions to exclude from execution")
|
|
250
250
|
max_actions_per_task: Optional[int] = Field(default=None, gt=0, description="Maximum actions per task")
|
|
251
251
|
display_files_in_done_text: Optional[bool] = Field(default=None, description="Whether to display files in done text")
|
|
252
252
|
|
|
253
253
|
class ControllerConfigResponse(BaseModel):
|
|
254
|
-
"""Response model for
|
|
254
|
+
"""Response model for tools configuration"""
|
|
255
255
|
exclude_actions: List[str] = []
|
|
256
256
|
max_actions_per_task: int = 100
|
|
257
257
|
display_files_in_done_text: bool = True
|
vibe_surf/backend/api/task.py
CHANGED
|
@@ -25,7 +25,9 @@ from ..shared_state import (
|
|
|
25
25
|
browser_manager
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
logger
|
|
28
|
+
from vibe_surf.logger import get_logger
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
29
31
|
|
|
30
32
|
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
|
31
33
|
|
|
@@ -76,16 +78,16 @@ async def submit_task(
|
|
|
76
78
|
task_id = uuid7str()
|
|
77
79
|
|
|
78
80
|
# Get MCP server config for saving
|
|
79
|
-
from ..shared_state import
|
|
81
|
+
from ..shared_state import vibesurf_tools, active_mcp_server
|
|
80
82
|
mcp_server_config = task_request.mcp_server_config
|
|
81
|
-
if not mcp_server_config and
|
|
82
|
-
mcp_server_config =
|
|
83
|
-
|
|
83
|
+
if not mcp_server_config and vibesurf_tools and hasattr(vibesurf_tools, 'mcp_server_config'):
|
|
84
|
+
mcp_server_config = vibesurf_tools.mcp_server_config
|
|
85
|
+
|
|
84
86
|
# Ensure we have a valid MCP server config (never None)
|
|
85
87
|
if mcp_server_config is None:
|
|
86
88
|
mcp_server_config = {"mcpServers": {}}
|
|
87
89
|
logger.info("Using default empty MCP server configuration")
|
|
88
|
-
|
|
90
|
+
|
|
89
91
|
# DEBUG: Log the type and content of mcp_server_config
|
|
90
92
|
logger.info(f"mcp_server_config type: {type(mcp_server_config)}, value: {mcp_server_config}")
|
|
91
93
|
|
|
@@ -140,16 +142,17 @@ async def _ensure_llm_initialized(llm_profile):
|
|
|
140
142
|
"""Ensure LLM is initialized with the specified profile"""
|
|
141
143
|
from ..utils.llm_factory import create_llm_from_profile
|
|
142
144
|
from ..shared_state import vibesurf_agent
|
|
143
|
-
|
|
145
|
+
|
|
144
146
|
if not vibesurf_agent:
|
|
145
147
|
raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
|
|
146
|
-
|
|
148
|
+
|
|
147
149
|
# Always create new LLM instance to ensure we're using the right profile
|
|
148
150
|
new_llm = create_llm_from_profile(llm_profile)
|
|
149
|
-
|
|
150
|
-
# Update vibesurf agent's LLM
|
|
151
|
-
vibesurf_agent
|
|
152
|
-
|
|
151
|
+
|
|
152
|
+
# Update vibesurf agent's LLM and register with token cost service
|
|
153
|
+
if vibesurf_agent and vibesurf_agent.token_cost_service:
|
|
154
|
+
vibesurf_agent.llm = vibesurf_agent.token_cost_service.register_llm(new_llm)
|
|
155
|
+
logger.info(f"LLM updated and registered for token tracking for profile: {llm_profile['profile_name']}")
|
|
153
156
|
|
|
154
157
|
|
|
155
158
|
@router.post("/pause")
|
|
@@ -260,6 +263,36 @@ async def stop_task(control_request: TaskControlRequest):
|
|
|
260
263
|
raise HTTPException(status_code=500, detail=f"Failed to stop task: {str(e)}")
|
|
261
264
|
|
|
262
265
|
|
|
266
|
+
@router.post("/add-new-task")
|
|
267
|
+
async def add_new_task(control_request: TaskControlRequest):
|
|
268
|
+
"""Add a new task or follow-up instruction during execution"""
|
|
269
|
+
from ..shared_state import vibesurf_agent
|
|
270
|
+
|
|
271
|
+
if not vibesurf_agent:
|
|
272
|
+
raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
|
|
273
|
+
|
|
274
|
+
if not is_task_running():
|
|
275
|
+
raise HTTPException(status_code=400, detail="No active task to add new instruction to")
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
# Use the reason field as the new task content
|
|
279
|
+
new_task = control_request.reason or "No additional task provided"
|
|
280
|
+
|
|
281
|
+
# Add the new task to the running agent
|
|
282
|
+
await vibesurf_agent.add_new_task(new_task)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
"success": True,
|
|
286
|
+
"message": "New task added successfully",
|
|
287
|
+
"operation": "add_new_task",
|
|
288
|
+
"new_task": new_task
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(f"Failed to add new task: {e}")
|
|
293
|
+
raise HTTPException(status_code=500, detail=f"Failed to add new task: {str(e)}")
|
|
294
|
+
|
|
295
|
+
|
|
263
296
|
@router.get("/detailed-status")
|
|
264
297
|
async def get_detailed_task_status():
|
|
265
298
|
"""Get detailed task execution status with vibesurf information"""
|