vibesurf 0.1.9a6__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 +878 -814
- vibe_surf/agents/views.py +130 -0
- vibe_surf/backend/api/activity.py +3 -1
- vibe_surf/backend/api/browser.py +70 -0
- 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 +47 -13
- 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 +80 -3
- vibe_surf/backend/shared_state.py +30 -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 +308 -62
- vibe_surf/browser/browser_manager.py +71 -100
- vibe_surf/browser/utils.py +5 -3
- vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
- vibe_surf/chrome_extension/background.js +88 -0
- vibe_surf/chrome_extension/manifest.json +3 -1
- vibe_surf/chrome_extension/scripts/api-client.js +13 -0
- vibe_surf/chrome_extension/scripts/file-manager.js +482 -0
- vibe_surf/chrome_extension/scripts/history-manager.js +658 -0
- vibe_surf/chrome_extension/scripts/modal-manager.js +487 -0
- vibe_surf/chrome_extension/scripts/session-manager.js +52 -11
- vibe_surf/chrome_extension/scripts/settings-manager.js +1214 -0
- vibe_surf/chrome_extension/scripts/ui-manager.js +1530 -3163
- vibe_surf/chrome_extension/sidepanel.html +47 -7
- vibe_surf/chrome_extension/styles/activity.css +934 -0
- vibe_surf/chrome_extension/styles/base.css +76 -0
- vibe_surf/chrome_extension/styles/history-modal.css +791 -0
- vibe_surf/chrome_extension/styles/input.css +568 -0
- vibe_surf/chrome_extension/styles/layout.css +186 -0
- vibe_surf/chrome_extension/styles/responsive.css +454 -0
- vibe_surf/chrome_extension/styles/settings-environment.css +165 -0
- vibe_surf/chrome_extension/styles/settings-forms.css +389 -0
- vibe_surf/chrome_extension/styles/settings-modal.css +141 -0
- vibe_surf/chrome_extension/styles/settings-profiles.css +244 -0
- vibe_surf/chrome_extension/styles/settings-responsive.css +144 -0
- vibe_surf/chrome_extension/styles/settings-utilities.css +25 -0
- vibe_surf/chrome_extension/styles/variables.css +54 -0
- vibe_surf/cli.py +5 -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 -221
- 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.9a6.dist-info → vibesurf-0.1.11.dist-info}/METADATA +23 -3
- vibesurf-0.1.11.dist-info/RECORD +93 -0
- vibe_surf/chrome_extension/styles/main.css +0 -2338
- vibe_surf/chrome_extension/styles/settings.css +0 -1100
- vibe_surf/controller/file_system.py +0 -53
- vibe_surf/controller/views.py +0 -37
- vibesurf-0.1.9a6.dist-info/RECORD +0 -71
- /vibe_surf/{controller → tools}/__init__.py +0 -0
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.9a6.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
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Browser Tabs Router
|
|
3
|
+
|
|
4
|
+
Handles retrieval of browser tab information including active tab and all tabs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from vibe_surf.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
router = APIRouter(prefix="/browser", tags=["browser"])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/active-tab")
|
|
19
|
+
async def get_active_tab() -> Dict[str, Dict[str, str]]:
|
|
20
|
+
from ..shared_state import browser_manager
|
|
21
|
+
"""Get the current active tab information"""
|
|
22
|
+
if not browser_manager:
|
|
23
|
+
raise HTTPException(status_code=503, detail="Browser manager not initialized")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# Get active tab info using browser manager
|
|
27
|
+
active_tab_info = await browser_manager.get_activate_tab()
|
|
28
|
+
|
|
29
|
+
if not active_tab_info:
|
|
30
|
+
logger.info("No active tab found!")
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
logger.info(active_tab_info)
|
|
34
|
+
# Return dict format: {tab_id: {url: , title: }}
|
|
35
|
+
return {
|
|
36
|
+
active_tab_info.target_id[:-4]: {
|
|
37
|
+
"url": active_tab_info.url,
|
|
38
|
+
"title": active_tab_info.title
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.error(f"Failed to get active tab: {e}")
|
|
44
|
+
raise HTTPException(status_code=500, detail=f"Failed to get active tab: {str(e)}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.get("/all-tabs")
|
|
48
|
+
async def get_all_tabs() -> Dict[str, Dict[str, str]]:
|
|
49
|
+
"""Get all browser tabs information"""
|
|
50
|
+
from ..shared_state import browser_manager
|
|
51
|
+
|
|
52
|
+
if not browser_manager:
|
|
53
|
+
raise HTTPException(status_code=503, detail="Browser manager not initialized")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
all_tab_infos = await browser_manager.get_all_tabs()
|
|
57
|
+
|
|
58
|
+
# Filter only page targets and build result dict
|
|
59
|
+
result = {}
|
|
60
|
+
for tab_info in all_tab_infos:
|
|
61
|
+
result[tab_info.target_id[-4:]] = {
|
|
62
|
+
"url": tab_info.url,
|
|
63
|
+
"title": tab_info.title
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Failed to get all tabs: {e}")
|
|
70
|
+
raise HTTPException(status_code=500, detail=f"Failed to get all tabs: {str(e)}")
|
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
|