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