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.

Files changed (70) hide show
  1. vibe_surf/__init__.py +12 -0
  2. vibe_surf/_version.py +34 -0
  3. vibe_surf/agents/__init__.py +0 -0
  4. vibe_surf/agents/browser_use_agent.py +1106 -0
  5. vibe_surf/agents/prompts/__init__.py +1 -0
  6. vibe_surf/agents/prompts/vibe_surf_prompt.py +176 -0
  7. vibe_surf/agents/report_writer_agent.py +360 -0
  8. vibe_surf/agents/vibe_surf_agent.py +1632 -0
  9. vibe_surf/backend/__init__.py +0 -0
  10. vibe_surf/backend/api/__init__.py +3 -0
  11. vibe_surf/backend/api/activity.py +243 -0
  12. vibe_surf/backend/api/config.py +740 -0
  13. vibe_surf/backend/api/files.py +322 -0
  14. vibe_surf/backend/api/models.py +257 -0
  15. vibe_surf/backend/api/task.py +300 -0
  16. vibe_surf/backend/database/__init__.py +13 -0
  17. vibe_surf/backend/database/manager.py +129 -0
  18. vibe_surf/backend/database/models.py +164 -0
  19. vibe_surf/backend/database/queries.py +922 -0
  20. vibe_surf/backend/database/schemas.py +100 -0
  21. vibe_surf/backend/llm_config.py +182 -0
  22. vibe_surf/backend/main.py +137 -0
  23. vibe_surf/backend/migrations/__init__.py +16 -0
  24. vibe_surf/backend/migrations/init_db.py +303 -0
  25. vibe_surf/backend/migrations/seed_data.py +236 -0
  26. vibe_surf/backend/shared_state.py +601 -0
  27. vibe_surf/backend/utils/__init__.py +7 -0
  28. vibe_surf/backend/utils/encryption.py +164 -0
  29. vibe_surf/backend/utils/llm_factory.py +225 -0
  30. vibe_surf/browser/__init__.py +8 -0
  31. vibe_surf/browser/agen_browser_profile.py +130 -0
  32. vibe_surf/browser/agent_browser_session.py +416 -0
  33. vibe_surf/browser/browser_manager.py +296 -0
  34. vibe_surf/browser/utils.py +790 -0
  35. vibe_surf/browser/watchdogs/__init__.py +0 -0
  36. vibe_surf/browser/watchdogs/action_watchdog.py +291 -0
  37. vibe_surf/browser/watchdogs/dom_watchdog.py +954 -0
  38. vibe_surf/chrome_extension/background.js +558 -0
  39. vibe_surf/chrome_extension/config.js +48 -0
  40. vibe_surf/chrome_extension/content.js +284 -0
  41. vibe_surf/chrome_extension/dev-reload.js +47 -0
  42. vibe_surf/chrome_extension/icons/convert-svg.js +33 -0
  43. vibe_surf/chrome_extension/icons/logo-preview.html +187 -0
  44. vibe_surf/chrome_extension/icons/logo.png +0 -0
  45. vibe_surf/chrome_extension/manifest.json +53 -0
  46. vibe_surf/chrome_extension/popup.html +134 -0
  47. vibe_surf/chrome_extension/scripts/api-client.js +473 -0
  48. vibe_surf/chrome_extension/scripts/main.js +491 -0
  49. vibe_surf/chrome_extension/scripts/markdown-it.min.js +3 -0
  50. vibe_surf/chrome_extension/scripts/session-manager.js +599 -0
  51. vibe_surf/chrome_extension/scripts/ui-manager.js +3687 -0
  52. vibe_surf/chrome_extension/sidepanel.html +347 -0
  53. vibe_surf/chrome_extension/styles/animations.css +471 -0
  54. vibe_surf/chrome_extension/styles/components.css +670 -0
  55. vibe_surf/chrome_extension/styles/main.css +2307 -0
  56. vibe_surf/chrome_extension/styles/settings.css +1100 -0
  57. vibe_surf/cli.py +357 -0
  58. vibe_surf/controller/__init__.py +0 -0
  59. vibe_surf/controller/file_system.py +53 -0
  60. vibe_surf/controller/mcp_client.py +68 -0
  61. vibe_surf/controller/vibesurf_controller.py +616 -0
  62. vibe_surf/controller/views.py +37 -0
  63. vibe_surf/llm/__init__.py +21 -0
  64. vibe_surf/llm/openai_compatible.py +237 -0
  65. vibesurf-0.1.0.dist-info/METADATA +97 -0
  66. vibesurf-0.1.0.dist-info/RECORD +70 -0
  67. vibesurf-0.1.0.dist-info/WHEEL +5 -0
  68. vibesurf-0.1.0.dist-info/entry_points.txt +2 -0
  69. vibesurf-0.1.0.dist-info/licenses/LICENSE +201 -0
  70. vibesurf-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,322 @@
1
+ """
2
+ File Upload and Management Router
3
+
4
+ Handles file uploads to workspace directories, file retrieval, and listing
5
+ of uploaded files for VibeSurf sessions.
6
+ """
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
9
+ from fastapi.responses import FileResponse
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+ from typing import List, Optional, Dict, Any
12
+ import os
13
+ import shutil
14
+ import logging
15
+ from datetime import datetime
16
+ from uuid_extensions import uuid7str
17
+ import mimetypes
18
+ from pathlib import Path
19
+
20
+ from ..database import get_db_session
21
+ from ..database.queries import UploadedFileQueries
22
+ from .models import FileListQueryRequest, SessionFilesQueryRequest
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ router = APIRouter(prefix="/files", tags=["files"])
27
+
28
+ def get_upload_directory(session_id: Optional[str] = None) -> str:
29
+ from ..shared_state import workspace_dir
30
+ """Get the upload directory path for a session or global uploads"""
31
+ if session_id:
32
+ upload_dir = os.path.join(workspace_dir, session_id, "upload_files")
33
+ else:
34
+ upload_dir = os.path.join(workspace_dir, "upload_files")
35
+
36
+ # Create directory if it doesn't exist
37
+ os.makedirs(upload_dir, exist_ok=True)
38
+ return upload_dir
39
+
40
+ def is_safe_path(basedir: str, path: str) -> bool:
41
+ """Check if the path is safe (within basedir)"""
42
+ try:
43
+ # Resolve both paths to absolute paths
44
+ basedir = os.path.abspath(basedir)
45
+ path = os.path.abspath(path)
46
+
47
+ # Check if path starts with basedir
48
+ return path.startswith(basedir)
49
+ except:
50
+ return False
51
+
52
+ @router.post("/upload")
53
+ async def upload_files(
54
+ files: List[UploadFile] = File(...),
55
+ session_id: Optional[str] = Form(None),
56
+ db: AsyncSession = Depends(get_db_session)
57
+ ):
58
+ """Upload files to workspace/upload_files folder or session-specific folder"""
59
+ try:
60
+ from ..shared_state import workspace_dir
61
+
62
+ upload_dir = get_upload_directory(session_id)
63
+ uploaded_file_info = []
64
+
65
+ for file in files:
66
+ if not file.filename:
67
+ continue
68
+
69
+ # Generate unique file ID
70
+ file_id = uuid7str()
71
+
72
+ # Create safe filename
73
+ filename = file.filename
74
+ file_path = os.path.join(upload_dir, filename)
75
+
76
+ # Handle duplicate filenames by adding suffix
77
+ counter = 1
78
+ base_name, ext = os.path.splitext(filename)
79
+ while os.path.exists(file_path):
80
+ new_filename = f"{base_name}_{counter}{ext}"
81
+ file_path = os.path.join(upload_dir, new_filename)
82
+ filename = new_filename
83
+ counter += 1
84
+
85
+ # Ensure path is safe
86
+ if not is_safe_path(upload_dir, file_path):
87
+ raise HTTPException(status_code=400, detail=f"Invalid file path: {filename}")
88
+
89
+ # Save file
90
+ try:
91
+ with open(file_path, "wb") as buffer:
92
+ shutil.copyfileobj(file.file, buffer)
93
+
94
+ # Get file info
95
+ file_size = os.path.getsize(file_path)
96
+ mime_type, _ = mimetypes.guess_type(file_path)
97
+ relative_path = os.path.relpath(file_path, workspace_dir)
98
+
99
+ # Store file metadata in database
100
+ uploaded_file = await UploadedFileQueries.create_file_record(
101
+ db=db,
102
+ file_id=file_id,
103
+ original_filename=file.filename,
104
+ stored_filename=filename,
105
+ file_path=file_path,
106
+ session_id=session_id,
107
+ file_size=file_size,
108
+ mime_type=mime_type or "application/octet-stream",
109
+ relative_path=relative_path
110
+ )
111
+
112
+ # Create response metadata
113
+ file_metadata = {
114
+ "file_id": uploaded_file.file_id,
115
+ "original_filename": uploaded_file.original_filename,
116
+ "stored_filename": uploaded_file.stored_filename,
117
+ "session_id": uploaded_file.session_id,
118
+ "file_size": uploaded_file.file_size,
119
+ "mime_type": uploaded_file.mime_type,
120
+ "upload_time": uploaded_file.upload_time.isoformat(),
121
+ "file_path": file_path
122
+ }
123
+
124
+ uploaded_file_info.append(file_metadata)
125
+
126
+ logger.info(f"File uploaded: {filename} (ID: {file_id}) to {upload_dir}")
127
+
128
+ except Exception as e:
129
+ logger.error(f"Failed to save file {filename}: {e}")
130
+ # If database record was created but file save failed, clean up
131
+ try:
132
+ await UploadedFileQueries.hard_delete_file(db, file_id)
133
+ except:
134
+ pass
135
+ raise HTTPException(status_code=500, detail=f"Failed to save file {filename}: {str(e)}")
136
+
137
+ # Commit all database changes
138
+ await db.commit()
139
+
140
+ return {
141
+ "message": f"Successfully uploaded {len(uploaded_file_info)} files",
142
+ "files": uploaded_file_info,
143
+ "upload_directory": upload_dir
144
+ }
145
+
146
+ except Exception as e:
147
+ logger.error(f"File upload failed: {e}")
148
+ raise HTTPException(status_code=500, detail=f"File upload failed: {str(e)}")
149
+
150
+ @router.get("/{file_id}")
151
+ async def download_file(file_id: str, db: AsyncSession = Depends(get_db_session)):
152
+ """Download file by file ID"""
153
+ from ..shared_state import workspace_dir
154
+
155
+ uploaded_file = await UploadedFileQueries.get_file(db, file_id)
156
+ if not uploaded_file:
157
+ raise HTTPException(status_code=404, detail="File not found")
158
+
159
+ file_path = uploaded_file.file_path
160
+
161
+ if not os.path.exists(file_path):
162
+ raise HTTPException(status_code=404, detail="File not found on disk")
163
+
164
+ # Ensure path is safe
165
+ if not is_safe_path(workspace_dir, file_path):
166
+ raise HTTPException(status_code=403, detail="Access denied")
167
+
168
+ return FileResponse(
169
+ path=file_path,
170
+ filename=uploaded_file.original_filename,
171
+ media_type=uploaded_file.mime_type
172
+ )
173
+
174
+ @router.get("")
175
+ async def list_uploaded_files(
176
+ query: FileListQueryRequest = Depends(),
177
+ db: AsyncSession = Depends(get_db_session)
178
+ ):
179
+ """List uploaded files, optionally filtered by session"""
180
+ try:
181
+ # Get files from database
182
+ uploaded_files = await UploadedFileQueries.list_files(
183
+ db=db,
184
+ session_id=query.session_id,
185
+ limit=query.limit,
186
+ offset=query.offset,
187
+ active_only=True
188
+ )
189
+
190
+ # Get total count
191
+ total_count = await UploadedFileQueries.count_files(
192
+ db=db,
193
+ session_id=query.session_id,
194
+ active_only=True
195
+ )
196
+
197
+ # Convert to response format (exclude file_path for security)
198
+ files_response = []
199
+ for file_record in uploaded_files:
200
+ files_response.append({
201
+ "file_id": file_record.file_id,
202
+ "original_filename": file_record.original_filename,
203
+ "stored_filename": file_record.stored_filename,
204
+ "session_id": file_record.session_id,
205
+ "file_size": file_record.file_size,
206
+ "mime_type": file_record.mime_type,
207
+ "upload_time": file_record.upload_time.isoformat(),
208
+ "file_path": file_record.file_path
209
+ })
210
+
211
+ return {
212
+ "files": files_response,
213
+ "total_count": total_count,
214
+ "limit": query.limit,
215
+ "offset": query.offset,
216
+ "has_more": query.limit != -1 and (query.offset + query.limit < total_count),
217
+ "session_id": query.session_id
218
+ }
219
+
220
+ except Exception as e:
221
+ logger.error(f"Failed to list files: {e}")
222
+ raise HTTPException(status_code=500, detail=f"Failed to list files: {str(e)}")
223
+
224
+ @router.delete("/{file_id}")
225
+ async def delete_file(file_id: str, db: AsyncSession = Depends(get_db_session)):
226
+ """Delete uploaded file by file ID"""
227
+ # Get file record
228
+ uploaded_file = await UploadedFileQueries.get_file(db, file_id)
229
+ if not uploaded_file:
230
+ raise HTTPException(status_code=404, detail="File not found")
231
+
232
+ try:
233
+ # Remove file from disk
234
+ if os.path.exists(uploaded_file.file_path):
235
+ os.remove(uploaded_file.file_path)
236
+
237
+ # Soft delete from database
238
+ success = await UploadedFileQueries.delete_file(db, file_id)
239
+ if not success:
240
+ raise HTTPException(status_code=500, detail="Failed to delete file record")
241
+
242
+ await db.commit()
243
+
244
+ return {
245
+ "message": f"File {uploaded_file.original_filename} deleted successfully",
246
+ "file_id": file_id
247
+ }
248
+
249
+ except HTTPException:
250
+ raise
251
+ except Exception as e:
252
+ logger.error(f"Failed to delete file {file_id}: {e}")
253
+ raise HTTPException(status_code=500, detail=f"Failed to delete file: {str(e)}")
254
+
255
+ @router.get("/session/{session_id}")
256
+ async def list_session_files(
257
+ session_id: str,
258
+ query: SessionFilesQueryRequest = Depends()
259
+ ):
260
+ """List all files in a session directory"""
261
+ try:
262
+ from ..shared_state import workspace_dir
263
+ session_dir = os.path.join(workspace_dir, session_id)
264
+
265
+ if not os.path.exists(session_dir):
266
+ return {
267
+ "session_id": session_id,
268
+ "files": [],
269
+ "directories": [],
270
+ "message": "Session directory not found"
271
+ }
272
+
273
+ files = []
274
+ directories = []
275
+
276
+ for root, dirs, filenames in os.walk(session_dir):
277
+ # Calculate relative path from session directory
278
+ rel_root = os.path.relpath(root, session_dir)
279
+ if rel_root == ".":
280
+ rel_root = ""
281
+
282
+ # Add directories if requested
283
+ if query.include_directories:
284
+ for dirname in dirs:
285
+ dir_path = os.path.join(rel_root, dirname) if rel_root else dirname
286
+ directories.append({
287
+ "name": dirname,
288
+ "path": dir_path,
289
+ "type": "directory"
290
+ })
291
+
292
+ # Add files
293
+ for filename in filenames:
294
+ file_path = os.path.join(root, filename)
295
+ rel_path = os.path.join(rel_root, filename) if rel_root else filename
296
+
297
+ try:
298
+ stat = os.stat(file_path)
299
+ mime_type, _ = mimetypes.guess_type(file_path)
300
+
301
+ files.append({
302
+ "name": filename,
303
+ "path": rel_path,
304
+ "size": stat.st_size,
305
+ "mime_type": mime_type or "application/octet-stream",
306
+ "modified_time": datetime.fromtimestamp(stat.st_mtime).isoformat(),
307
+ "type": "file"
308
+ })
309
+ except Exception as e:
310
+ logger.warning(f"Could not get stats for file {file_path}: {e}")
311
+
312
+ return {
313
+ "session_id": session_id,
314
+ "files": files,
315
+ "directories": directories if query.include_directories else [],
316
+ "total_files": len(files),
317
+ "total_directories": len(directories) if query.include_directories else 0
318
+ }
319
+
320
+ except Exception as e:
321
+ 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)}")
@@ -0,0 +1,257 @@
1
+ """
2
+ API Request/Response Models for VibeSurf Backend
3
+
4
+ Pydantic models for API serialization and validation.
5
+ With LLM Profile management support.
6
+ """
7
+
8
+ from pydantic import BaseModel, Field, validator
9
+ from typing import List, Optional, Dict, Any
10
+ from datetime import datetime
11
+ from enum import Enum
12
+
13
+ # LLM Profile Models
14
+ class LLMProfileCreateRequest(BaseModel):
15
+ """Request model for creating a new LLM profile"""
16
+ profile_name: str = Field(description="Unique profile name", min_length=1, max_length=100)
17
+ provider: str = Field(description="LLM provider (openai, anthropic, google, azure_openai)")
18
+ model: str = Field(description="Model name")
19
+ api_key: Optional[str] = Field(default=None, description="API key (will be encrypted)")
20
+ base_url: Optional[str] = Field(default=None, description="Custom base URL")
21
+ temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
22
+ max_tokens: Optional[int] = Field(default=None, gt=0)
23
+ top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0)
24
+ frequency_penalty: Optional[float] = Field(default=None, ge=-2.0, le=2.0)
25
+ seed: Optional[int] = Field(default=None)
26
+ provider_config: Optional[Dict[str, Any]] = Field(default=None, description="Provider-specific config")
27
+ description: Optional[str] = Field(default=None, description="Profile description")
28
+ is_default: bool = Field(default=False, description="Set as default profile")
29
+
30
+ class LLMProfileUpdateRequest(BaseModel):
31
+ """Request model for updating an LLM profile"""
32
+ provider: Optional[str] = None
33
+ model: Optional[str] = None
34
+ api_key: Optional[str] = None
35
+ base_url: Optional[str] = None
36
+ temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
37
+ max_tokens: Optional[int] = Field(default=None, gt=0)
38
+ top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0)
39
+ frequency_penalty: Optional[float] = Field(default=None, ge=-2.0, le=2.0)
40
+ seed: Optional[int] = None
41
+ provider_config: Optional[Dict[str, Any]] = None
42
+ description: Optional[str] = None
43
+ is_active: Optional[bool] = None
44
+ is_default: Optional[bool] = None
45
+
46
+ class LLMProfileResponse(BaseModel):
47
+ """Response model for LLM profile data (without API key)"""
48
+ profile_id: str
49
+ profile_name: str
50
+ provider: str
51
+ model: str
52
+ base_url: Optional[str] = None
53
+ # Note: API key is intentionally excluded from response
54
+ temperature: Optional[float] = None
55
+ max_tokens: Optional[int] = None
56
+ top_p: Optional[float] = None
57
+ frequency_penalty: Optional[float] = None
58
+ seed: Optional[int] = None
59
+ provider_config: Optional[Dict[str, Any]] = None
60
+ description: Optional[str] = None
61
+ is_active: bool
62
+ is_default: bool
63
+ created_at: datetime
64
+ updated_at: datetime
65
+ last_used_at: Optional[datetime] = None
66
+
67
+ class Config:
68
+ from_attributes = True
69
+
70
+ # MCP Profile Models
71
+ class McpProfileCreateRequest(BaseModel):
72
+ """Request model for creating a new MCP profile"""
73
+ display_name: str = Field(description="Display name for MCP profile", min_length=1, max_length=100)
74
+ mcp_server_name: str = Field(description="MCP server name/identifier", min_length=1, max_length=100)
75
+ mcp_server_params: Dict[str, Any] = Field(description="MCP server parameters (command, args, etc.)")
76
+ description: Optional[str] = Field(default=None, description="Profile description")
77
+
78
+ class McpProfileUpdateRequest(BaseModel):
79
+ """Request model for updating an MCP profile"""
80
+ display_name: Optional[str] = Field(default=None, min_length=1, max_length=100)
81
+ mcp_server_name: Optional[str] = Field(default=None, min_length=1, max_length=100)
82
+ mcp_server_params: Optional[Dict[str, Any]] = None
83
+ description: Optional[str] = None
84
+ is_active: Optional[bool] = None
85
+
86
+ class McpProfileResponse(BaseModel):
87
+ """Response model for MCP profile data"""
88
+ mcp_id: str
89
+ display_name: str
90
+ mcp_server_name: str
91
+ mcp_server_params: Dict[str, Any]
92
+ description: Optional[str] = None
93
+ is_active: bool
94
+ created_at: datetime
95
+ updated_at: datetime
96
+ last_used_at: Optional[datetime] = None
97
+
98
+ class Config:
99
+ from_attributes = True
100
+
101
+ # Task Models
102
+ class TaskCreateRequest(BaseModel):
103
+ """Request model for creating a new task"""
104
+ session_id: str = Field(description="Session identifier")
105
+ task_description: str = Field(description="The task description")
106
+ llm_profile_name: str = Field(description="LLM profile name to use")
107
+ upload_files_path: Optional[str] = Field(default=None, description="Path to uploaded files")
108
+ mcp_server_config: Optional[Dict[str, Any]] = Field(default=None, description="MCP server configuration")
109
+
110
+ class TaskControlRequest(BaseModel):
111
+ """Request model for task control operations (pause/resume/stop)"""
112
+ reason: Optional[str] = Field(default=None, description="Reason for the operation")
113
+
114
+ class TaskResponse(BaseModel):
115
+ """Response model for task data"""
116
+ task_id: str
117
+ session_id: str
118
+ task_description: str
119
+ status: str
120
+ llm_profile_name: str
121
+ upload_files_path: Optional[str] = None
122
+ workspace_dir: Optional[str] = None
123
+ mcp_server_config: Optional[Dict[str, Any]] = None
124
+ task_result: Optional[str] = None
125
+ error_message: Optional[str] = None
126
+ report_path: Optional[str] = None
127
+ created_at: datetime
128
+ updated_at: datetime
129
+ started_at: Optional[datetime] = None
130
+ completed_at: Optional[datetime] = None
131
+ task_metadata: Optional[Dict[str, Any]] = None
132
+
133
+ class Config:
134
+ from_attributes = True
135
+
136
+ @classmethod
137
+ def from_orm(cls, task):
138
+ """Create response from SQLAlchemy Task model"""
139
+ return cls(
140
+ task_id=task.task_id,
141
+ session_id=task.session_id,
142
+ task_description=task.task_description,
143
+ status=task.status.value,
144
+ llm_profile_name=task.llm_profile_name,
145
+ upload_files_path=task.upload_files_path,
146
+ workspace_dir=task.workspace_dir,
147
+ mcp_server_config=task.mcp_server_config,
148
+ task_result=task.task_result,
149
+ error_message=task.error_message,
150
+ report_path=task.report_path,
151
+ created_at=task.created_at,
152
+ updated_at=task.updated_at,
153
+ started_at=task.started_at,
154
+ completed_at=task.completed_at,
155
+ task_metadata=task.task_metadata
156
+ )
157
+
158
+ class TaskStatusResponse(BaseModel):
159
+ """Response model for task status information"""
160
+ task_id: Optional[str] = None
161
+ session_id: Optional[str] = None
162
+ status: Optional[str] = None
163
+ task_description: Optional[str] = None
164
+ created_at: Optional[datetime] = None
165
+ started_at: Optional[datetime] = None
166
+ error_message: Optional[str] = None
167
+ is_running: bool = False
168
+
169
+ class TaskListResponse(BaseModel):
170
+ """Response model for task list"""
171
+ tasks: List[TaskResponse]
172
+ total_count: int
173
+ session_id: Optional[str] = None
174
+
175
+ class ErrorResponse(BaseModel):
176
+ """Standard error response model"""
177
+ error: str
178
+ detail: Optional[str] = None
179
+ timestamp: datetime = Field(default_factory=datetime.now)
180
+
181
+ class ControlOperationResponse(BaseModel):
182
+ """Response model for control operations"""
183
+ success: bool
184
+ message: str
185
+ operation: str
186
+ timestamp: datetime
187
+ details: Optional[Dict[str, Any]] = None
188
+
189
+ # Activity Log Models (for VibeSurf agent activity)
190
+ class ActivityLogEntry(BaseModel):
191
+ """Model for VibeSurf agent activity log entry"""
192
+ timestamp: datetime
193
+ level: str
194
+ message: str
195
+ metadata: Optional[Dict[str, Any]] = None
196
+
197
+ class ActivityLogResponse(BaseModel):
198
+ """Response model for activity logs"""
199
+ logs: List[ActivityLogEntry]
200
+ total_count: int
201
+ session_id: Optional[str] = None
202
+ task_id: Optional[str] = None
203
+
204
+ # Activity API Request Models
205
+ class ActivityQueryRequest(BaseModel):
206
+ """Request model for getting recent tasks"""
207
+ limit: int = Field(default=-1, ge=-1, le=1000, description="Number of recent tasks to retrieve (-1 for all)")
208
+
209
+ class SessionActivityQueryRequest(BaseModel):
210
+ """Request model for getting session activity logs"""
211
+ limit: int = Field(default=-1, ge=-1, le=1000, description="Number of activity logs to retrieve (-1 for all)")
212
+ message_index: Optional[int] = Field(default=None, ge=0, description="Specific message index to retrieve")
213
+
214
+ # File API Request Models
215
+ class FileUploadRequest(BaseModel):
216
+ """Request model for file upload (for form validation)"""
217
+ session_id: Optional[str] = Field(default=None, description="Session ID for file association")
218
+
219
+ class FileListQueryRequest(BaseModel):
220
+ """Request model for listing uploaded files"""
221
+ session_id: Optional[str] = Field(default=None, description="Filter by session ID")
222
+ limit: int = Field(default=-1, ge=-1, le=1000, description="Number of files to retrieve (-1 for all)")
223
+ offset: int = Field(default=0, ge=0, description="Number of files to skip")
224
+
225
+ class SessionFilesQueryRequest(BaseModel):
226
+ """Request model for listing session files"""
227
+ include_directories: bool = Field(default=False, description="Whether to include directories in the response")
228
+
229
+ # File Upload Models
230
+ class UploadedFileResponse(BaseModel):
231
+ """Response model for uploaded file information"""
232
+ filename: str
233
+ file_path: str
234
+ file_size: Optional[int] = None
235
+ mime_type: Optional[str] = None
236
+ uploaded_at: datetime
237
+
238
+ # Configuration Models (for config endpoints)
239
+ class LLMConfigResponse(BaseModel):
240
+ """Response model for LLM configuration"""
241
+ provider: str
242
+ model: str
243
+ temperature: Optional[float] = None
244
+ max_tokens: Optional[int] = None
245
+ available_providers: List[str] = []
246
+
247
+ class ControllerConfigRequest(BaseModel):
248
+ """Request model for updating controller configuration"""
249
+ exclude_actions: Optional[List[str]] = Field(default=None, description="Actions to exclude from execution")
250
+ max_actions_per_task: Optional[int] = Field(default=None, gt=0, description="Maximum actions per task")
251
+ display_files_in_done_text: Optional[bool] = Field(default=None, description="Whether to display files in done text")
252
+
253
+ class ControllerConfigResponse(BaseModel):
254
+ """Response model for controller configuration"""
255
+ exclude_actions: List[str] = []
256
+ max_actions_per_task: int = 100
257
+ display_files_in_done_text: bool = True