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.
- vibe_surf/__init__.py +12 -0
- vibe_surf/_version.py +34 -0
- vibe_surf/agents/__init__.py +0 -0
- vibe_surf/agents/browser_use_agent.py +1106 -0
- vibe_surf/agents/prompts/__init__.py +1 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +176 -0
- vibe_surf/agents/report_writer_agent.py +360 -0
- vibe_surf/agents/vibe_surf_agent.py +1632 -0
- vibe_surf/backend/__init__.py +0 -0
- vibe_surf/backend/api/__init__.py +3 -0
- vibe_surf/backend/api/activity.py +243 -0
- vibe_surf/backend/api/config.py +740 -0
- vibe_surf/backend/api/files.py +322 -0
- vibe_surf/backend/api/models.py +257 -0
- vibe_surf/backend/api/task.py +300 -0
- vibe_surf/backend/database/__init__.py +13 -0
- vibe_surf/backend/database/manager.py +129 -0
- vibe_surf/backend/database/models.py +164 -0
- vibe_surf/backend/database/queries.py +922 -0
- vibe_surf/backend/database/schemas.py +100 -0
- vibe_surf/backend/llm_config.py +182 -0
- vibe_surf/backend/main.py +137 -0
- vibe_surf/backend/migrations/__init__.py +16 -0
- vibe_surf/backend/migrations/init_db.py +303 -0
- vibe_surf/backend/migrations/seed_data.py +236 -0
- vibe_surf/backend/shared_state.py +601 -0
- vibe_surf/backend/utils/__init__.py +7 -0
- vibe_surf/backend/utils/encryption.py +164 -0
- vibe_surf/backend/utils/llm_factory.py +225 -0
- vibe_surf/browser/__init__.py +8 -0
- vibe_surf/browser/agen_browser_profile.py +130 -0
- vibe_surf/browser/agent_browser_session.py +416 -0
- vibe_surf/browser/browser_manager.py +296 -0
- vibe_surf/browser/utils.py +790 -0
- vibe_surf/browser/watchdogs/__init__.py +0 -0
- vibe_surf/browser/watchdogs/action_watchdog.py +291 -0
- vibe_surf/browser/watchdogs/dom_watchdog.py +954 -0
- vibe_surf/chrome_extension/background.js +558 -0
- vibe_surf/chrome_extension/config.js +48 -0
- vibe_surf/chrome_extension/content.js +284 -0
- vibe_surf/chrome_extension/dev-reload.js +47 -0
- vibe_surf/chrome_extension/icons/convert-svg.js +33 -0
- vibe_surf/chrome_extension/icons/logo-preview.html +187 -0
- vibe_surf/chrome_extension/icons/logo.png +0 -0
- vibe_surf/chrome_extension/manifest.json +53 -0
- vibe_surf/chrome_extension/popup.html +134 -0
- vibe_surf/chrome_extension/scripts/api-client.js +473 -0
- vibe_surf/chrome_extension/scripts/main.js +491 -0
- vibe_surf/chrome_extension/scripts/markdown-it.min.js +3 -0
- vibe_surf/chrome_extension/scripts/session-manager.js +599 -0
- vibe_surf/chrome_extension/scripts/ui-manager.js +3687 -0
- vibe_surf/chrome_extension/sidepanel.html +347 -0
- vibe_surf/chrome_extension/styles/animations.css +471 -0
- vibe_surf/chrome_extension/styles/components.css +670 -0
- vibe_surf/chrome_extension/styles/main.css +2307 -0
- vibe_surf/chrome_extension/styles/settings.css +1100 -0
- vibe_surf/cli.py +357 -0
- vibe_surf/controller/__init__.py +0 -0
- vibe_surf/controller/file_system.py +53 -0
- vibe_surf/controller/mcp_client.py +68 -0
- vibe_surf/controller/vibesurf_controller.py +616 -0
- vibe_surf/controller/views.py +37 -0
- vibe_surf/llm/__init__.py +21 -0
- vibe_surf/llm/openai_compatible.py +237 -0
- vibesurf-0.1.0.dist-info/METADATA +97 -0
- vibesurf-0.1.0.dist-info/RECORD +70 -0
- vibesurf-0.1.0.dist-info/WHEEL +5 -0
- vibesurf-0.1.0.dist-info/entry_points.txt +2 -0
- vibesurf-0.1.0.dist-info/licenses/LICENSE +201 -0
- vibesurf-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1632 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import pdb
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import pickle
|
|
13
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
14
|
+
from uuid_extensions import uuid7str
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from langgraph.graph import StateGraph, END
|
|
17
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
from json_repair import repair_json
|
|
20
|
+
|
|
21
|
+
from browser_use.browser.session import BrowserSession
|
|
22
|
+
from browser_use.llm.base import BaseChatModel
|
|
23
|
+
from browser_use.llm.messages import UserMessage, SystemMessage, BaseMessage, AssistantMessage
|
|
24
|
+
from browser_use.browser.views import TabInfo
|
|
25
|
+
|
|
26
|
+
from vibe_surf.agents.browser_use_agent import BrowserUseAgent
|
|
27
|
+
from vibe_surf.agents.report_writer_agent import ReportWriterAgent
|
|
28
|
+
|
|
29
|
+
from vibe_surf.agents.prompts.vibe_surf_prompt import (
|
|
30
|
+
SUPERVISOR_AGENT_SYSTEM_PROMPT,
|
|
31
|
+
)
|
|
32
|
+
from vibe_surf.browser.browser_manager import BrowserManager
|
|
33
|
+
from vibe_surf.controller.vibesurf_controller import VibeSurfController
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TodoItem(BaseModel):
|
|
39
|
+
"""Individual todo item with simple string-based task description"""
|
|
40
|
+
task: str = Field(description="Simple task description")
|
|
41
|
+
status: Literal["pending", "in_progress", "completed"] = "pending"
|
|
42
|
+
assigned_agent_id: Optional[str] = None
|
|
43
|
+
result: Optional[str] = None
|
|
44
|
+
error: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExecutionMode(BaseModel):
|
|
48
|
+
"""Execution mode configuration"""
|
|
49
|
+
mode: Literal["single", "parallel"] = "single"
|
|
50
|
+
max_parallel_agents: int = 5
|
|
51
|
+
reason: str = Field(description="LLM reasoning for mode selection")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BrowserTaskResult(BaseModel):
|
|
55
|
+
"""Result from browser task execution"""
|
|
56
|
+
agent_id: str
|
|
57
|
+
task: str
|
|
58
|
+
success: bool
|
|
59
|
+
result: Optional[str] = None
|
|
60
|
+
error: Optional[str] = None
|
|
61
|
+
screenshots: List[str] = Field(default_factory=list)
|
|
62
|
+
extracted_data: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ReportRequirement(BaseModel):
|
|
66
|
+
"""Indicates if and what type of report is needed"""
|
|
67
|
+
needs_report: bool = False
|
|
68
|
+
report_type: Literal["summary", "detailed", "none"] = "none"
|
|
69
|
+
reason: str = Field(description="LLM reasoning for report decision")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ControlResult(BaseModel):
|
|
73
|
+
"""Result of a control operation"""
|
|
74
|
+
success: bool
|
|
75
|
+
message: str
|
|
76
|
+
timestamp: datetime = Field(default_factory=datetime.now)
|
|
77
|
+
details: Optional[Dict[str, Any]] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AgentStatus(BaseModel):
|
|
81
|
+
"""Status of an individual agent"""
|
|
82
|
+
agent_id: str
|
|
83
|
+
status: Literal["running", "paused", "stopped", "idle", "error"] = "idle"
|
|
84
|
+
current_action: Optional[str] = None
|
|
85
|
+
last_update: datetime = Field(default_factory=datetime.now)
|
|
86
|
+
error_message: Optional[str] = None
|
|
87
|
+
pause_reason: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class VibeSurfStatus(BaseModel):
|
|
91
|
+
"""Overall status of the vibesurf execution"""
|
|
92
|
+
overall_status: Literal["running", "paused", "stopped", "idle", "error"] = "idle"
|
|
93
|
+
agent_statuses: Dict[str, AgentStatus] = Field(default_factory=dict)
|
|
94
|
+
progress: Dict[str, Any] = Field(default_factory=dict)
|
|
95
|
+
last_update: datetime = Field(default_factory=datetime.now)
|
|
96
|
+
active_step: Optional[str] = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class VibeSurfState:
|
|
101
|
+
"""Main LangGraph state for VibeSurfAgent workflow with simplified architecture"""
|
|
102
|
+
|
|
103
|
+
# Core task information
|
|
104
|
+
original_task: str = ""
|
|
105
|
+
upload_files: List[str] = field(default_factory=list)
|
|
106
|
+
session_id: str = field(default_factory=lambda: uuid7str())
|
|
107
|
+
task_id: str = field(default_factory=lambda: uuid7str())
|
|
108
|
+
|
|
109
|
+
# Workflow state
|
|
110
|
+
current_step: str = "task_analysis"
|
|
111
|
+
is_simple_response: bool = False
|
|
112
|
+
|
|
113
|
+
# Supervisor Agent - Core controller with message history
|
|
114
|
+
supervisor_message_history: List[BaseMessage] = field(default_factory=list)
|
|
115
|
+
supervisor_action: Optional[str] = None
|
|
116
|
+
|
|
117
|
+
# Todo list management
|
|
118
|
+
todo_list: List[TodoItem] = field(default_factory=list)
|
|
119
|
+
completed_todos: List[TodoItem] = field(default_factory=list)
|
|
120
|
+
current_task_index: int = 0
|
|
121
|
+
|
|
122
|
+
# Task execution
|
|
123
|
+
execution_mode: Optional[ExecutionMode] = None
|
|
124
|
+
pending_tasks: List[str] = field(default_factory=list)
|
|
125
|
+
pending_todo_indices: List[int] = field(default_factory=list) # Track which todo indices are being executed
|
|
126
|
+
browser_results: List[BrowserTaskResult] = field(default_factory=list) # record all browser result
|
|
127
|
+
prev_browser_results: List[BrowserTaskResult] = field(default_factory=list) # record previous browser result
|
|
128
|
+
|
|
129
|
+
# Response outputs
|
|
130
|
+
simple_response: Optional[str] = None
|
|
131
|
+
generated_report_path: Optional[str] = None
|
|
132
|
+
final_summary: Optional[str] = None
|
|
133
|
+
is_complete: bool = False
|
|
134
|
+
|
|
135
|
+
# File organization
|
|
136
|
+
workspace_dir: str = "./workspace"
|
|
137
|
+
session_dir: Optional[str] = None
|
|
138
|
+
task_dir: Optional[str] = None
|
|
139
|
+
|
|
140
|
+
# Integration components
|
|
141
|
+
browser_manager: Optional[BrowserManager] = None
|
|
142
|
+
vibesurf_controller: Optional[VibeSurfController] = None
|
|
143
|
+
llm: Optional[BaseChatModel] = None
|
|
144
|
+
vibesurf_agent: Optional[Any] = None
|
|
145
|
+
|
|
146
|
+
# Control state management
|
|
147
|
+
paused: bool = False
|
|
148
|
+
stopped: bool = False
|
|
149
|
+
should_pause: bool = False
|
|
150
|
+
should_stop: bool = False
|
|
151
|
+
|
|
152
|
+
# Agent control tracking
|
|
153
|
+
agent_control_states: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
154
|
+
paused_agents: set = field(default_factory=set)
|
|
155
|
+
|
|
156
|
+
# Control metadata
|
|
157
|
+
control_timestamps: Dict[str, datetime] = field(default_factory=dict)
|
|
158
|
+
control_reasons: Dict[str, str] = field(default_factory=dict)
|
|
159
|
+
last_control_action: Optional[str] = None
|
|
160
|
+
|
|
161
|
+
# Agent activity log
|
|
162
|
+
agent_activity_logs: List[Dict[str, str]] = field(default_factory=list)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Utility functions for parsing LLM JSON responses
|
|
166
|
+
def parse_json_response(response_text: str, fallback_data: Dict) -> Dict:
|
|
167
|
+
"""Parse JSON response with repair capability"""
|
|
168
|
+
try:
|
|
169
|
+
# Try to find JSON in the response
|
|
170
|
+
json_start = response_text.find('{')
|
|
171
|
+
json_end = response_text.rfind('}') + 1
|
|
172
|
+
|
|
173
|
+
if json_start >= 0 and json_end > json_start:
|
|
174
|
+
json_text = response_text[json_start:json_end]
|
|
175
|
+
try:
|
|
176
|
+
return json.loads(json_text)
|
|
177
|
+
except json.JSONDecodeError:
|
|
178
|
+
# Try to repair JSON
|
|
179
|
+
repaired_json = repair_json(json_text)
|
|
180
|
+
return json.loads(repaired_json)
|
|
181
|
+
|
|
182
|
+
# If no JSON found, return fallback
|
|
183
|
+
return fallback_data
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.warning(f"JSON parsing failed: {e}, using fallback")
|
|
186
|
+
return fallback_data
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def parse_task_analysis_response(response_text: str) -> Dict:
|
|
190
|
+
"""Parse task analysis JSON response for simple response detection"""
|
|
191
|
+
fallback = {
|
|
192
|
+
"is_simple_response": False,
|
|
193
|
+
"reasoning": "Failed to parse response, defaulting to complex task",
|
|
194
|
+
"simple_response_content": None
|
|
195
|
+
}
|
|
196
|
+
return parse_json_response(response_text, fallback)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_supervisor_response(response_text: str) -> Dict:
|
|
200
|
+
"""Parse supervisor agent JSON response"""
|
|
201
|
+
fallback = {
|
|
202
|
+
"action": "summary_generation",
|
|
203
|
+
"reasoning": "Failed to parse response, defaulting to summary generation",
|
|
204
|
+
"todo_items": [],
|
|
205
|
+
"task_type": "single",
|
|
206
|
+
"tasks_to_execute": [],
|
|
207
|
+
"summary_content": None
|
|
208
|
+
}
|
|
209
|
+
result = parse_json_response(response_text, fallback)
|
|
210
|
+
|
|
211
|
+
# Ensure todo_items is always a list for actions that modify todos
|
|
212
|
+
if result.get("action") in ["generate_todos", "update_todos"]:
|
|
213
|
+
if "todo_items" not in result or not isinstance(result["todo_items"], list):
|
|
214
|
+
result["todo_items"] = []
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def parse_todo_generation_response(response_text: str) -> List[str]:
|
|
220
|
+
"""Parse todo generation JSON response"""
|
|
221
|
+
fallback = {
|
|
222
|
+
"todo_items": ["Complete the original task"]
|
|
223
|
+
}
|
|
224
|
+
result = parse_json_response(response_text, fallback)
|
|
225
|
+
return result.get("todo_items", fallback["todo_items"])[:5] # Max 5 items
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def parse_execution_planning_response(response_text: str) -> ExecutionMode:
|
|
229
|
+
"""Parse execution planning JSON response"""
|
|
230
|
+
fallback = {
|
|
231
|
+
"execution_mode": "single",
|
|
232
|
+
"max_parallel_agents": 3,
|
|
233
|
+
"reasoning": "Default single mode"
|
|
234
|
+
}
|
|
235
|
+
result = parse_json_response(response_text, fallback)
|
|
236
|
+
|
|
237
|
+
return ExecutionMode(
|
|
238
|
+
mode=result.get("execution_mode", "single"),
|
|
239
|
+
max_parallel_agents=result.get("max_parallel_agents", 3),
|
|
240
|
+
reason=result.get("reasoning", "Default execution mode")
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def parse_todo_update_response(response_text: str) -> Dict:
|
|
245
|
+
"""Parse todo update JSON response"""
|
|
246
|
+
fallback = {
|
|
247
|
+
"additional_tasks": [],
|
|
248
|
+
"next_action": "summary_generation",
|
|
249
|
+
"reasoning": "Default action"
|
|
250
|
+
}
|
|
251
|
+
return parse_json_response(response_text, fallback)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def parse_report_decision_response(response_text: str) -> ReportRequirement:
|
|
255
|
+
"""Parse report decision JSON response"""
|
|
256
|
+
fallback = {
|
|
257
|
+
"needs_report": False,
|
|
258
|
+
"report_type": "none",
|
|
259
|
+
"reasoning": "Default no report"
|
|
260
|
+
}
|
|
261
|
+
result = parse_json_response(response_text, fallback)
|
|
262
|
+
|
|
263
|
+
return ReportRequirement(
|
|
264
|
+
needs_report=result.get("needs_report", False),
|
|
265
|
+
report_type=result.get("report_type", "none"),
|
|
266
|
+
reason=result.get("reasoning", "Default report decision")
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def format_upload_files_list(upload_files: Optional[List[str]] = None) -> str:
|
|
271
|
+
"""Format uploaded file list for LLM prompt"""
|
|
272
|
+
if upload_files is None:
|
|
273
|
+
return ""
|
|
274
|
+
return "\n".join(
|
|
275
|
+
[f"{i + 1}. [{os.path.basename(file_path)}](file:///{file_path})" for i, file_path in enumerate(upload_files)])
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def format_todo_list(todo_list: List[TodoItem]) -> str:
|
|
279
|
+
"""Format todo list for LLM prompt"""
|
|
280
|
+
return "\n".join([f"{i + 1}. {item.task}" for i, item in enumerate(todo_list)])
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def format_completed_todos(completed_todos: List[TodoItem]) -> str:
|
|
284
|
+
"""Format completed todos for LLM prompt"""
|
|
285
|
+
return "\n".join([f"✅ {item.task} - {item.result or 'Completed'}" for item in completed_todos])
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def format_browser_results(browser_results: List[BrowserTaskResult]) -> str:
|
|
289
|
+
"""Format browser results for LLM prompt"""
|
|
290
|
+
result_text = []
|
|
291
|
+
for result in browser_results:
|
|
292
|
+
status = "✅ Success" if result.success else "❌ Failed"
|
|
293
|
+
result_text.append(f"{status}: {result.task}")
|
|
294
|
+
if result.result:
|
|
295
|
+
result_text.append(f" Result: {result.result}...")
|
|
296
|
+
if result.error:
|
|
297
|
+
result_text.append(f" Error: {result.error}")
|
|
298
|
+
return "\n".join(result_text)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def format_todo_list_markdown(todo_list: List[TodoItem]) -> str:
|
|
302
|
+
"""Format todo list as markdown"""
|
|
303
|
+
if not todo_list:
|
|
304
|
+
return "No todo items"
|
|
305
|
+
|
|
306
|
+
markdown_lines = []
|
|
307
|
+
for i, item in enumerate(todo_list):
|
|
308
|
+
if item.status == "completed":
|
|
309
|
+
status_symbol = "- [x] "
|
|
310
|
+
else:
|
|
311
|
+
status_symbol = "- [ ] "
|
|
312
|
+
markdown_lines.append(f"{status_symbol}{item.task}")
|
|
313
|
+
|
|
314
|
+
return "\n".join(markdown_lines)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def log_agent_activity(state: VibeSurfState, agent_name: str, agent_status: str, agent_msg: str) -> None:
|
|
318
|
+
"""Log agent activity to the activity log"""
|
|
319
|
+
activity_entry = {
|
|
320
|
+
"agent_name": agent_name,
|
|
321
|
+
"agent_status": agent_status, # working, result, error
|
|
322
|
+
"agent_msg": agent_msg
|
|
323
|
+
}
|
|
324
|
+
state.agent_activity_logs.append(activity_entry)
|
|
325
|
+
logger.info(f"📝 Logged activity: {agent_name} - {agent_status}:\n{agent_msg}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def create_browser_agent_step_callback(state: VibeSurfState, agent_name: str):
|
|
329
|
+
"""Create a step callback function for browser-use agent to log each step"""
|
|
330
|
+
|
|
331
|
+
def step_callback(browser_state_summary, agent_output, step_num: int) -> None:
|
|
332
|
+
"""Callback function to log browser agent step information"""
|
|
333
|
+
try:
|
|
334
|
+
# Format step information as markdown
|
|
335
|
+
step_msg = f"## Step {step_num}\n\n"
|
|
336
|
+
|
|
337
|
+
# Add thinking if present
|
|
338
|
+
if agent_output.thinking:
|
|
339
|
+
step_msg += f"**💡 Thinking:**\n{agent_output.thinking}\n\n"
|
|
340
|
+
|
|
341
|
+
# Add evaluation if present
|
|
342
|
+
if agent_output.evaluation_previous_goal:
|
|
343
|
+
step_msg += f"**👍 Evaluation:**\n{agent_output.evaluation_previous_goal}\n\n"
|
|
344
|
+
|
|
345
|
+
# Add memory if present
|
|
346
|
+
# if agent_output.memory:
|
|
347
|
+
# step_msg += f"**🧠 Memory:** {agent_output.memory}\n\n"
|
|
348
|
+
|
|
349
|
+
# Add next goal if present
|
|
350
|
+
if agent_output.next_goal:
|
|
351
|
+
step_msg += f"**🎯 Next Goal:**\n{agent_output.next_goal}\n\n"
|
|
352
|
+
|
|
353
|
+
# Add action summary
|
|
354
|
+
if agent_output.action and len(agent_output.action) > 0:
|
|
355
|
+
action_count = len(agent_output.action)
|
|
356
|
+
step_msg += f"**⚡ Actions:**\n"
|
|
357
|
+
|
|
358
|
+
# Add brief action details
|
|
359
|
+
for i, action in enumerate(agent_output.action): # Limit to first 3 actions to avoid too much detail
|
|
360
|
+
action_data = action.model_dump(exclude_unset=True)
|
|
361
|
+
action_name = next(iter(action_data.keys())) if action_data else 'unknown'
|
|
362
|
+
action_params = json.dumps(action_data[action_name], ensure_ascii=False) if action_name in action_data else ""
|
|
363
|
+
step_msg += f"- [x] {action_name}: {action_params}\n"
|
|
364
|
+
else:
|
|
365
|
+
step_msg += f"**⚡ Actions:** No actions\n"
|
|
366
|
+
|
|
367
|
+
# Log the step activity
|
|
368
|
+
log_agent_activity(state, agent_name, "working", step_msg.strip())
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"❌ Error in step callback for {agent_name}: {e}")
|
|
372
|
+
# Log a simple fallback message
|
|
373
|
+
log_agent_activity(state, agent_name, "step", f"Step {step_num} completed")
|
|
374
|
+
|
|
375
|
+
return step_callback
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def ensure_directories(state: VibeSurfState) -> None:
|
|
379
|
+
"""Ensure proper directory structure"""
|
|
380
|
+
# Create session directory: workspace_dir/session_id/
|
|
381
|
+
state.session_dir = os.path.join(state.workspace_dir, state.session_id)
|
|
382
|
+
os.makedirs(state.session_dir, exist_ok=True)
|
|
383
|
+
|
|
384
|
+
# Create task directory: workspace_dir/session_id/task_id/
|
|
385
|
+
state.task_dir = os.path.join(state.session_dir, state.task_id)
|
|
386
|
+
os.makedirs(state.task_dir, exist_ok=True)
|
|
387
|
+
|
|
388
|
+
# Create subdirectories for different output types
|
|
389
|
+
os.makedirs(os.path.join(state.task_dir, "screenshots"), exist_ok=True)
|
|
390
|
+
os.makedirs(os.path.join(state.task_dir, "reports"), exist_ok=True)
|
|
391
|
+
os.makedirs(os.path.join(state.task_dir, "logs"), exist_ok=True)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# Control-aware node wrapper
|
|
395
|
+
async def control_aware_node(node_func, state: VibeSurfState, node_name: str) -> VibeSurfState:
|
|
396
|
+
"""
|
|
397
|
+
Wrapper for workflow nodes that adds control state checking
|
|
398
|
+
"""
|
|
399
|
+
# Check control state before executing node
|
|
400
|
+
if state.stopped:
|
|
401
|
+
logger.info(f"🛑 Node {node_name} skipped - workflow stopped")
|
|
402
|
+
return state
|
|
403
|
+
|
|
404
|
+
# Handle pause state
|
|
405
|
+
while state.paused or state.should_pause:
|
|
406
|
+
if not state.paused and state.should_pause:
|
|
407
|
+
logger.info(f"⏸️ Node {node_name} pausing workflow")
|
|
408
|
+
state.paused = True
|
|
409
|
+
state.should_pause = False
|
|
410
|
+
state.control_timestamps["paused"] = datetime.now()
|
|
411
|
+
|
|
412
|
+
logger.debug(f"⏸️ Node {node_name} waiting - workflow paused")
|
|
413
|
+
await asyncio.sleep(0.5) # Check every 500ms
|
|
414
|
+
|
|
415
|
+
# Allow stopping while paused
|
|
416
|
+
if state.stopped or state.should_stop:
|
|
417
|
+
logger.info(f"🛑 Node {node_name} stopped while paused")
|
|
418
|
+
state.stopped = True
|
|
419
|
+
state.should_stop = False
|
|
420
|
+
state.control_timestamps["stopped"] = datetime.now()
|
|
421
|
+
return state
|
|
422
|
+
|
|
423
|
+
# Check for stop signal
|
|
424
|
+
if state.should_stop:
|
|
425
|
+
logger.info(f"🛑 Node {node_name} stopping workflow")
|
|
426
|
+
state.stopped = True
|
|
427
|
+
state.should_stop = False
|
|
428
|
+
state.control_timestamps["stopped"] = datetime.now()
|
|
429
|
+
return state
|
|
430
|
+
|
|
431
|
+
# Execute the actual node
|
|
432
|
+
logger.debug(f"▶️ Executing node: {node_name}")
|
|
433
|
+
state.last_control_action = f"executing_{node_name}"
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
return await node_func(state)
|
|
437
|
+
except Exception as e:
|
|
438
|
+
logger.error(f"❌ Node {node_name} failed: {e}")
|
|
439
|
+
raise
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# LangGraph Nodes
|
|
443
|
+
|
|
444
|
+
async def supervisor_agent_node(state: VibeSurfState) -> VibeSurfState:
|
|
445
|
+
"""
|
|
446
|
+
Core supervisor agent node - manages todos, assigns tasks, and coordinates workflow
|
|
447
|
+
"""
|
|
448
|
+
return await control_aware_node(_supervisor_agent_node_impl, state, "supervisor_agent")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def format_browser_tabs(tabs: Optional[List[TabInfo]] = None) -> str:
|
|
452
|
+
if not tabs:
|
|
453
|
+
return ""
|
|
454
|
+
return "\n".join([f"[{i}] Page Title: {item.title}, Page Url: {item.url}, Page ID: {item.target_id}" for i, item in enumerate(tabs)])
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
async def _supervisor_agent_node_impl(state: VibeSurfState) -> VibeSurfState:
|
|
458
|
+
"""Implementation of supervisor agent node - core workflow controller"""
|
|
459
|
+
logger.info("🎯 Supervisor Agent: Managing workflow and task coordination...")
|
|
460
|
+
|
|
461
|
+
supervisor_message_history = state.supervisor_message_history
|
|
462
|
+
|
|
463
|
+
# Build supervisor user prompt with current context
|
|
464
|
+
if state.prev_browser_results:
|
|
465
|
+
browser_results_md = format_browser_results(state.prev_browser_results)
|
|
466
|
+
supervisor_message_history.append(AssistantMessage(
|
|
467
|
+
content=f"Previous Browser Execution Results: \n{browser_results_md}"))
|
|
468
|
+
elif state.generated_report_path:
|
|
469
|
+
supervisor_message_history.append(AssistantMessage(content=f"Generated Report Path: {state.generated_report_path}"))
|
|
470
|
+
|
|
471
|
+
if state.todo_list:
|
|
472
|
+
supervisor_message_history.append(UserMessage(
|
|
473
|
+
content=f"Completed Todos:\n{format_completed_todos(state.completed_todos)}\nCurrent Todos:\n{format_todo_list(state.todo_list)}"))
|
|
474
|
+
|
|
475
|
+
browser_tabs = await state.browser_manager.main_browser_session.get_tabs()
|
|
476
|
+
browser_tabs_md = format_browser_tabs(browser_tabs)
|
|
477
|
+
if browser_tabs_md:
|
|
478
|
+
supervisor_message_history.append(UserMessage(
|
|
479
|
+
content=f"Available Browser Tabs:\n{browser_tabs_md}"))
|
|
480
|
+
|
|
481
|
+
# Reset prev_browser_results
|
|
482
|
+
state.prev_browser_results = []
|
|
483
|
+
try:
|
|
484
|
+
response = await state.llm.ainvoke(supervisor_message_history)
|
|
485
|
+
# add result to message history
|
|
486
|
+
supervisor_message_history.append(AssistantMessage(content=response.completion))
|
|
487
|
+
|
|
488
|
+
supervisor_result = parse_supervisor_response(response.completion)
|
|
489
|
+
|
|
490
|
+
action = supervisor_result["action"]
|
|
491
|
+
reasoning = supervisor_result["reasoning"]
|
|
492
|
+
# Log agent activity
|
|
493
|
+
log_agent_activity(state, "supervisor_agent", "thinking", f"{reasoning}")
|
|
494
|
+
|
|
495
|
+
state.supervisor_action = action
|
|
496
|
+
|
|
497
|
+
logger.info(f"🎯 Supervisor decision: {action} - {reasoning}")
|
|
498
|
+
|
|
499
|
+
# Handle different actions
|
|
500
|
+
if action == "generate_todos":
|
|
501
|
+
# Generate initial todos
|
|
502
|
+
todo_items = supervisor_result.get("todo_items", [])
|
|
503
|
+
if todo_items:
|
|
504
|
+
state.todo_list = [TodoItem(task=task) for task in todo_items]
|
|
505
|
+
todo_todo_list_md = format_todo_list_markdown(state.todo_list)
|
|
506
|
+
supervisor_message_history.append(
|
|
507
|
+
UserMessage(content=f"Successfully generated todo list:\n{todo_todo_list_md}"))
|
|
508
|
+
log_agent_activity(state, "supervisor_agent", "result", f"Todo List:\n\n{todo_todo_list_md}")
|
|
509
|
+
# Continue in supervisor to assign tasks
|
|
510
|
+
state.current_step = "supervisor_agent"
|
|
511
|
+
|
|
512
|
+
elif action == "update_todos":
|
|
513
|
+
# Replace all remaining todos with the new list
|
|
514
|
+
todo_items = supervisor_result.get("todo_items", [])
|
|
515
|
+
if todo_items:
|
|
516
|
+
# Clear current todo list and replace with new items
|
|
517
|
+
state.todo_list = [TodoItem(task=task) for task in todo_items]
|
|
518
|
+
todo_todo_list_md = format_todo_list_markdown(state.completed_todos + state.todo_list)
|
|
519
|
+
supervisor_message_history.append(
|
|
520
|
+
UserMessage(content=f"Successfully Updated todo list:\n{todo_todo_list_md}"))
|
|
521
|
+
log_agent_activity(state, "supervisor_agent", "result", f"Todo List:\n\n{todo_todo_list_md}")
|
|
522
|
+
else:
|
|
523
|
+
# If no todo_items provided, clear the list
|
|
524
|
+
state.todo_list = []
|
|
525
|
+
todo_todo_list_md = format_todo_list_markdown(state.completed_todos + state.todo_list)
|
|
526
|
+
supervisor_message_history.append(
|
|
527
|
+
UserMessage(content=f"Cleared todo list - all tasks completed:\n{todo_todo_list_md}"))
|
|
528
|
+
log_agent_activity(state, "supervisor_agent", "result",
|
|
529
|
+
f"Cleared todo list - all tasks completed\n{todo_todo_list_md}")
|
|
530
|
+
|
|
531
|
+
# Continue in supervisor to assign tasks
|
|
532
|
+
state.current_step = "supervisor_agent"
|
|
533
|
+
|
|
534
|
+
elif action == "assign_browser_task":
|
|
535
|
+
# Assign browser tasks
|
|
536
|
+
task_type = supervisor_result.get("task_type", "single")
|
|
537
|
+
tasks_to_execute = supervisor_result.get("tasks_to_execute", [])
|
|
538
|
+
|
|
539
|
+
if tasks_to_execute:
|
|
540
|
+
tasks_to_execute_new = []
|
|
541
|
+
todo_indices = [] # Track which todo items are being executed
|
|
542
|
+
|
|
543
|
+
for task_item in tasks_to_execute:
|
|
544
|
+
if isinstance(task_item, list):
|
|
545
|
+
# Format: [page_index, todo_index]
|
|
546
|
+
page_index, todo_index = task_item
|
|
547
|
+
if todo_index < len(state.todo_list):
|
|
548
|
+
task_description = state.todo_list[todo_index].task
|
|
549
|
+
tasks_to_execute_new.append([browser_tabs[page_index].target_id, task_description])
|
|
550
|
+
todo_indices.append(todo_index)
|
|
551
|
+
else:
|
|
552
|
+
# Format: todo_index
|
|
553
|
+
todo_index = task_item
|
|
554
|
+
if todo_index < len(state.todo_list):
|
|
555
|
+
task_description = state.todo_list[todo_index].task
|
|
556
|
+
tasks_to_execute_new.append(task_description)
|
|
557
|
+
todo_indices.append(todo_index)
|
|
558
|
+
|
|
559
|
+
state.execution_mode = ExecutionMode(
|
|
560
|
+
mode=task_type,
|
|
561
|
+
max_parallel_agents=5 if task_type == "parallel" else 1,
|
|
562
|
+
reason=reasoning
|
|
563
|
+
)
|
|
564
|
+
state.pending_tasks = tasks_to_execute_new
|
|
565
|
+
state.pending_todo_indices = todo_indices # Store which todo indices are being executed
|
|
566
|
+
state.current_step = "browser_task_execution"
|
|
567
|
+
|
|
568
|
+
log_agent_activity(state, "supervisor_agent", "result",
|
|
569
|
+
f"Assigned {len(tasks_to_execute)} browser tasks ({task_type} mode)")
|
|
570
|
+
else:
|
|
571
|
+
# No tasks to execute, continue in supervisor
|
|
572
|
+
state.current_step = "supervisor_agent"
|
|
573
|
+
supervisor_message_history.append(
|
|
574
|
+
UserMessage(content=f"No tasks to execute. Please provide browser tasks to execute."))
|
|
575
|
+
|
|
576
|
+
elif action == "assign_report_task":
|
|
577
|
+
# Assign report generation task
|
|
578
|
+
state.current_step = "report_task_execution"
|
|
579
|
+
log_agent_activity(state, "supervisor_agent", "result", "Assigned report generation task")
|
|
580
|
+
|
|
581
|
+
elif action == "simple_response":
|
|
582
|
+
# Use provided content or generate if not provided
|
|
583
|
+
state.current_step = "simple_response"
|
|
584
|
+
state.simple_response = supervisor_result["simple_response_content"]
|
|
585
|
+
state.is_complete = True
|
|
586
|
+
log_agent_activity(state, "supervisor_agent", "result", state.simple_response)
|
|
587
|
+
elif action == "summary_generation":
|
|
588
|
+
# Handle summary generation directly in supervisor
|
|
589
|
+
summary_content = supervisor_result.get("summary_content")
|
|
590
|
+
|
|
591
|
+
if summary_content:
|
|
592
|
+
# Use LLM-provided summary content
|
|
593
|
+
state.final_summary = summary_content
|
|
594
|
+
state.is_complete = True
|
|
595
|
+
state.current_step = "summary_generation"
|
|
596
|
+
log_agent_activity(state, "supervisor_agent", "result", f"{summary_content}")
|
|
597
|
+
else:
|
|
598
|
+
# Generate summary using the same logic as the old summary generation node
|
|
599
|
+
state.current_step = "supervisor_agent"
|
|
600
|
+
supervisor_message_history.append(
|
|
601
|
+
UserMessage(content=f"The summary content is empty. Please provide summary content if you think all requirements have been accomplished."))
|
|
602
|
+
else:
|
|
603
|
+
# Unknown action, default to complete workflow
|
|
604
|
+
state.current_step = "summary_generation"
|
|
605
|
+
log_agent_activity(state, "supervisor_agent", "error", f"Unknown action: {action}")
|
|
606
|
+
|
|
607
|
+
return state
|
|
608
|
+
|
|
609
|
+
except Exception as e:
|
|
610
|
+
logger.error(f"❌ Supervisor agent failed: {e}")
|
|
611
|
+
state.current_step = "summary_generation"
|
|
612
|
+
log_agent_activity(state, "supervisor_agent", "error", f"Supervisor failed: {str(e)}")
|
|
613
|
+
return state
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
async def browser_task_execution_node(state: VibeSurfState) -> VibeSurfState:
|
|
617
|
+
"""
|
|
618
|
+
Execute browser tasks assigned by supervisor agent
|
|
619
|
+
"""
|
|
620
|
+
return await control_aware_node(_browser_task_execution_node_impl, state, "browser_task_execution")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
async def _browser_task_execution_node_impl(state: VibeSurfState) -> VibeSurfState:
|
|
624
|
+
"""Implementation of browser task execution node"""
|
|
625
|
+
logger.info("🚀 Executing browser tasks assigned by supervisor...")
|
|
626
|
+
|
|
627
|
+
# Log agent activity
|
|
628
|
+
log_agent_activity(state, "browser_task_executor", "working",
|
|
629
|
+
f"Executing {len(state.pending_tasks)} browser tasks in {state.execution_mode.mode if state.execution_mode else 'single'} mode")
|
|
630
|
+
|
|
631
|
+
# Setup file organization
|
|
632
|
+
ensure_directories(state)
|
|
633
|
+
|
|
634
|
+
try:
|
|
635
|
+
if state.execution_mode and state.execution_mode.mode == "parallel":
|
|
636
|
+
# Execute tasks in parallel
|
|
637
|
+
results = await execute_parallel_browser_tasks(state)
|
|
638
|
+
else:
|
|
639
|
+
# Execute tasks in single mode
|
|
640
|
+
results = await execute_single_browser_tasks(state)
|
|
641
|
+
|
|
642
|
+
# Update browser results
|
|
643
|
+
state.prev_browser_results = copy.deepcopy(results)
|
|
644
|
+
state.browser_results.extend(results)
|
|
645
|
+
|
|
646
|
+
# Mark corresponding todos as completed using indices
|
|
647
|
+
for i, todo_index in enumerate(state.pending_todo_indices):
|
|
648
|
+
if todo_index < len(state.todo_list) and state.todo_list[todo_index].status != "completed":
|
|
649
|
+
todo = state.todo_list[todo_index]
|
|
650
|
+
todo.status = "completed"
|
|
651
|
+
if i < len(results):
|
|
652
|
+
todo.result = results[i].result if results[i].success else None
|
|
653
|
+
todo.error = results[i].error if not results[i].success else None
|
|
654
|
+
state.completed_todos.append(todo)
|
|
655
|
+
|
|
656
|
+
# Remove completed todos from the todo list
|
|
657
|
+
# Sort indices in reverse order to avoid index shifting issues
|
|
658
|
+
for todo_index in sorted(state.pending_todo_indices, reverse=True):
|
|
659
|
+
if todo_index < len(state.todo_list):
|
|
660
|
+
state.todo_list.pop(todo_index)
|
|
661
|
+
|
|
662
|
+
# Clear pending tasks and indices
|
|
663
|
+
state.pending_tasks = []
|
|
664
|
+
state.pending_todo_indices = []
|
|
665
|
+
|
|
666
|
+
# Return to supervisor for next decision
|
|
667
|
+
state.current_step = "supervisor_agent"
|
|
668
|
+
|
|
669
|
+
# Log result
|
|
670
|
+
successful_tasks = sum(1 for result in results if result.success)
|
|
671
|
+
log_agent_activity(state, "browser_task_executor", "result",
|
|
672
|
+
f"Browser execution completed: {successful_tasks}/{len(results)} tasks successful")
|
|
673
|
+
|
|
674
|
+
logger.info(f"✅ Browser task execution completed with {len(results)} results")
|
|
675
|
+
return state
|
|
676
|
+
|
|
677
|
+
except Exception as e:
|
|
678
|
+
logger.error(f"❌ Browser task execution failed: {e}")
|
|
679
|
+
|
|
680
|
+
# Create error results for pending tasks
|
|
681
|
+
error_results = []
|
|
682
|
+
for i, task in enumerate(state.pending_tasks):
|
|
683
|
+
# Get the actual task description for the error result
|
|
684
|
+
if isinstance(task, list):
|
|
685
|
+
task_description = task[1] # [target_id, task_description]
|
|
686
|
+
else:
|
|
687
|
+
task_description = task
|
|
688
|
+
error_results.append(BrowserTaskResult(
|
|
689
|
+
agent_id="error",
|
|
690
|
+
task=task_description,
|
|
691
|
+
success=False,
|
|
692
|
+
error=str(e)
|
|
693
|
+
))
|
|
694
|
+
|
|
695
|
+
state.browser_results.extend(error_results)
|
|
696
|
+
state.pending_tasks = []
|
|
697
|
+
state.pending_todo_indices = []
|
|
698
|
+
state.current_step = "supervisor_agent"
|
|
699
|
+
|
|
700
|
+
log_agent_activity(state, "browser_task_executor", "error", f"Browser execution failed: {str(e)}")
|
|
701
|
+
return state
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
async def report_task_execution_node(state: VibeSurfState) -> VibeSurfState:
|
|
705
|
+
"""
|
|
706
|
+
Execute HTML report generation task assigned by supervisor agent
|
|
707
|
+
"""
|
|
708
|
+
return await control_aware_node(_report_task_execution_node_impl, state, "report_task_execution")
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
async def _report_task_execution_node_impl(state: VibeSurfState) -> VibeSurfState:
|
|
712
|
+
"""Implementation of report task execution node"""
|
|
713
|
+
logger.info("📄 Executing HTML report generation task...")
|
|
714
|
+
|
|
715
|
+
# Log agent activity
|
|
716
|
+
log_agent_activity(state, "report_task_executor", "working", "Generating HTML report")
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
# Use ReportWriterAgent to generate HTML report
|
|
720
|
+
report_writer = ReportWriterAgent(
|
|
721
|
+
llm=state.llm,
|
|
722
|
+
workspace_dir=state.task_dir
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
report_data = {
|
|
726
|
+
"original_task": state.original_task,
|
|
727
|
+
"execution_results": state.browser_results,
|
|
728
|
+
"report_type": "detailed", # Default to detailed report
|
|
729
|
+
"upload_files": state.upload_files
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
report_path = await report_writer.generate_report(report_data)
|
|
733
|
+
|
|
734
|
+
state.generated_report_path = report_path
|
|
735
|
+
|
|
736
|
+
# Return to supervisor for next decision
|
|
737
|
+
state.current_step = "supervisor_agent"
|
|
738
|
+
|
|
739
|
+
log_agent_activity(state, "report_task_executor", "result",
|
|
740
|
+
f"HTML report generated successfully at: `{report_path}`")
|
|
741
|
+
|
|
742
|
+
logger.info(f"✅ Report generated: {report_path}")
|
|
743
|
+
return state
|
|
744
|
+
|
|
745
|
+
except Exception as e:
|
|
746
|
+
logger.error(f"❌ Report generation failed: {e}")
|
|
747
|
+
state.current_step = "supervisor_agent"
|
|
748
|
+
log_agent_activity(state, "report_task_executor", "error", f"Report generation failed: {str(e)}")
|
|
749
|
+
return state
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
async def execute_parallel_browser_tasks(state: VibeSurfState) -> List[BrowserTaskResult]:
|
|
753
|
+
"""Execute pending tasks in parallel using multiple browser agents"""
|
|
754
|
+
logger.info("🔄 Executing pending tasks in parallel...")
|
|
755
|
+
|
|
756
|
+
# Register agents with browser manager
|
|
757
|
+
agents = []
|
|
758
|
+
pending_tasks = state.pending_tasks[:state.execution_mode.max_parallel_agents]
|
|
759
|
+
bu_agent_ids = []
|
|
760
|
+
register_sessions = []
|
|
761
|
+
for i, task in enumerate(pending_tasks):
|
|
762
|
+
agent_id = f"agent-{i + 1}-{state.task_id[-4:]}"
|
|
763
|
+
if isinstance(task, list):
|
|
764
|
+
target_id, task_description = task
|
|
765
|
+
else:
|
|
766
|
+
task_description = task
|
|
767
|
+
target_id = None
|
|
768
|
+
register_sessions.append(
|
|
769
|
+
state.browser_manager.register_agent(agent_id, target_id=target_id)
|
|
770
|
+
)
|
|
771
|
+
bu_agent_ids.append(agent_id)
|
|
772
|
+
agent_browser_sessions = await asyncio.gather(*register_sessions)
|
|
773
|
+
|
|
774
|
+
for i, task in enumerate(pending_tasks):
|
|
775
|
+
agent_id = f"agent-{i + 1}-{state.task_id[-4:]}"
|
|
776
|
+
if isinstance(task, list):
|
|
777
|
+
target_id, task_description = task
|
|
778
|
+
else:
|
|
779
|
+
task_description = task
|
|
780
|
+
try:
|
|
781
|
+
# Log agent creation
|
|
782
|
+
log_agent_activity(state, f"browser_use_agent-{i + 1}-{state.task_id[-4:]}", "working",
|
|
783
|
+
f"{task_description}")
|
|
784
|
+
|
|
785
|
+
# Create BrowserUseAgent for each task
|
|
786
|
+
if state.upload_files:
|
|
787
|
+
upload_files_md = format_upload_files_list(state.upload_files)
|
|
788
|
+
bu_task = task_description + f"\nAvailable uploaded files:\n{upload_files_md}\n"
|
|
789
|
+
else:
|
|
790
|
+
bu_task = task_description
|
|
791
|
+
|
|
792
|
+
# Create step callback for this agent
|
|
793
|
+
agent_name = f"browser_use_agent-{i + 1}-{state.task_id[-4:]}"
|
|
794
|
+
step_callback = create_browser_agent_step_callback(state, agent_name)
|
|
795
|
+
|
|
796
|
+
agent = BrowserUseAgent(
|
|
797
|
+
task=bu_task,
|
|
798
|
+
llm=state.llm,
|
|
799
|
+
browser_session=agent_browser_sessions[i],
|
|
800
|
+
controller=state.vibesurf_controller,
|
|
801
|
+
task_id=f"{state.task_id}-{i + 1}",
|
|
802
|
+
file_system_path=state.task_dir,
|
|
803
|
+
register_new_step_callback=step_callback,
|
|
804
|
+
extend_system_message="Please make sure the language of your output in JSON value should remain the same as the user's request or task."
|
|
805
|
+
)
|
|
806
|
+
agents.append(agent)
|
|
807
|
+
|
|
808
|
+
# Track agent in VibeSurfAgent for control coordination
|
|
809
|
+
if state.vibesurf_agent and hasattr(state.vibesurf_agent, '_running_agents'):
|
|
810
|
+
state.vibesurf_agent._running_agents[agent_id] = agent
|
|
811
|
+
logger.debug(f"🔗 Registered parallel agent {agent_id} for control coordination")
|
|
812
|
+
|
|
813
|
+
except Exception as e:
|
|
814
|
+
logger.error(f"❌ Failed to create agent {agent_id}: {e}")
|
|
815
|
+
log_agent_activity(state, f"browser_use_agent-{i + 1}-{state.task_id[-4:]}", "error",
|
|
816
|
+
f"Failed to create agent: {str(e)}")
|
|
817
|
+
|
|
818
|
+
# Execute all agents in parallel
|
|
819
|
+
try:
|
|
820
|
+
histories = await asyncio.gather(*[agent.run() for agent in agents], return_exceptions=True)
|
|
821
|
+
|
|
822
|
+
# Process results
|
|
823
|
+
results = []
|
|
824
|
+
for i, (agent, history) in enumerate(zip(agents, histories)):
|
|
825
|
+
agent_id = f"agent-{i + 1}-{state.task_id[-4:]}"
|
|
826
|
+
if isinstance(history, Exception):
|
|
827
|
+
results.append(BrowserTaskResult(
|
|
828
|
+
agent_id=f"agent-{i + 1}",
|
|
829
|
+
task=pending_tasks[i],
|
|
830
|
+
success=False,
|
|
831
|
+
error=str(history)
|
|
832
|
+
))
|
|
833
|
+
# Log error
|
|
834
|
+
log_agent_activity(state, f"browser_use_agent-{i + 1}-{state.task_id[-4:]}", "error",
|
|
835
|
+
f"Task failed: {str(history)}")
|
|
836
|
+
else:
|
|
837
|
+
results.append(BrowserTaskResult(
|
|
838
|
+
agent_id=f"agent-{i + 1}",
|
|
839
|
+
task=pending_tasks[i],
|
|
840
|
+
success=history.is_successful(),
|
|
841
|
+
result=history.final_result() if hasattr(history, 'final_result') else "Task completed",
|
|
842
|
+
error=str(history.errors()) if history.has_errors() and not history.is_successful() else ""
|
|
843
|
+
))
|
|
844
|
+
# Log result
|
|
845
|
+
if history.is_successful():
|
|
846
|
+
result_text = history.final_result() if hasattr(history, 'final_result') else "Task completed"
|
|
847
|
+
log_agent_activity(state, f"browser_use_agent-{i + 1}-{state.task_id[-4:]}", "result",
|
|
848
|
+
f"Task completed successfully: \n{result_text}")
|
|
849
|
+
else:
|
|
850
|
+
error_text = str(history.errors()) if history.has_errors() else "Unknown error"
|
|
851
|
+
log_agent_activity(state, f"browser_use_agent-{i + 1}-{state.task_id[-4:]}", "error",
|
|
852
|
+
f"Task failed: {error_text}")
|
|
853
|
+
|
|
854
|
+
return results
|
|
855
|
+
|
|
856
|
+
finally:
|
|
857
|
+
# Remove agents from control tracking and cleanup browser sessions
|
|
858
|
+
for i, agent_id in enumerate(bu_agent_ids):
|
|
859
|
+
if not isinstance(pending_tasks[i], list):
|
|
860
|
+
await state.browser_manager.unregister_agent(agent_id, close_tabs=True)
|
|
861
|
+
if state.vibesurf_agent and hasattr(state.vibesurf_agent, '_running_agents'):
|
|
862
|
+
state.vibesurf_agent._running_agents.pop(agent_id, None)
|
|
863
|
+
logger.debug(f"🔗 Unregistered parallel agent {agent_id} from control coordination")
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
async def execute_single_browser_tasks(state: VibeSurfState) -> List[BrowserTaskResult]:
|
|
867
|
+
"""Execute pending tasks in single mode one by one"""
|
|
868
|
+
logger.info("🔄 Executing pending tasks in single mode...")
|
|
869
|
+
|
|
870
|
+
results = []
|
|
871
|
+
for i, task in enumerate(state.pending_tasks):
|
|
872
|
+
if isinstance(task, list):
|
|
873
|
+
target_id, task_description = task
|
|
874
|
+
else:
|
|
875
|
+
task_description = task
|
|
876
|
+
logger.info(f"🔄 Executing task ({i + 1}/{len(state.pending_tasks)}): {task_description}")
|
|
877
|
+
|
|
878
|
+
agent_id = f"agent-single-{state.task_id[-4:]}-{i}"
|
|
879
|
+
|
|
880
|
+
# Log agent activity
|
|
881
|
+
log_agent_activity(state, f"browser_use_agent-{state.task_id[-4:]}", "working", f"{task_description}")
|
|
882
|
+
|
|
883
|
+
try:
|
|
884
|
+
await state.browser_manager._get_active_target()
|
|
885
|
+
if state.upload_files:
|
|
886
|
+
upload_files_md = format_upload_files_list(state.upload_files)
|
|
887
|
+
bu_task = task_description + f"\nAvailable user uploaded files:\n{upload_files_md}\n"
|
|
888
|
+
else:
|
|
889
|
+
bu_task = task_description
|
|
890
|
+
# Create step callback for this agent
|
|
891
|
+
agent_name = f"browser_use_agent-{state.task_id[-4:]}"
|
|
892
|
+
step_callback = create_browser_agent_step_callback(state, agent_name)
|
|
893
|
+
|
|
894
|
+
agent = BrowserUseAgent(
|
|
895
|
+
task=bu_task,
|
|
896
|
+
llm=state.llm,
|
|
897
|
+
browser_session=state.browser_manager.main_browser_session,
|
|
898
|
+
controller=state.vibesurf_controller,
|
|
899
|
+
task_id=f"{state.task_id}-{i}",
|
|
900
|
+
file_system_path=state.task_dir,
|
|
901
|
+
register_new_step_callback=step_callback,
|
|
902
|
+
extend_system_message="Please make sure the language of your output in JSON values should remain the same as the user's request or task."
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
# Track agent in VibeSurfAgent for control coordination
|
|
906
|
+
if state.vibesurf_agent and hasattr(state.vibesurf_agent, '_running_agents'):
|
|
907
|
+
state.vibesurf_agent._running_agents[agent_id] = agent
|
|
908
|
+
logger.debug(f"🔗 Registered single agent {agent_id} for control coordination")
|
|
909
|
+
|
|
910
|
+
try:
|
|
911
|
+
history = await agent.run()
|
|
912
|
+
|
|
913
|
+
result = BrowserTaskResult(
|
|
914
|
+
agent_id=agent_id,
|
|
915
|
+
task=task,
|
|
916
|
+
success=history.is_successful(),
|
|
917
|
+
result=history.final_result() if hasattr(history, 'final_result') else "Task completed",
|
|
918
|
+
error=str(history.errors()) if history.has_errors() and not history.is_successful() else ""
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
# Log result
|
|
922
|
+
if result.success:
|
|
923
|
+
log_agent_activity(state, f"browser_use_agent-{state.task_id[-4:]}", "result",
|
|
924
|
+
f"Task completed successfully: \n{result.result}")
|
|
925
|
+
else:
|
|
926
|
+
log_agent_activity(state, f"browser_use_agent-{state.task_id[-4:]}", "error",
|
|
927
|
+
f"Task failed: {result.error}")
|
|
928
|
+
|
|
929
|
+
results.append(result)
|
|
930
|
+
finally:
|
|
931
|
+
# Remove agent from control tracking
|
|
932
|
+
if state.vibesurf_agent and hasattr(state.vibesurf_agent, '_running_agents'):
|
|
933
|
+
state.vibesurf_agent._running_agents.pop(agent_id, None)
|
|
934
|
+
logger.debug(f"🔗 Unregistered single agent {agent_id} from control coordination")
|
|
935
|
+
|
|
936
|
+
except Exception as e:
|
|
937
|
+
logger.error(f"❌ Single task execution failed: {e}")
|
|
938
|
+
log_agent_activity(state, f"browser_use_agent-{state.task_id[-4:]}", "error",
|
|
939
|
+
f"Task execution failed: {str(e)}")
|
|
940
|
+
results.append(BrowserTaskResult(
|
|
941
|
+
agent_id=agent_id,
|
|
942
|
+
task=task,
|
|
943
|
+
success=False,
|
|
944
|
+
error=str(e)
|
|
945
|
+
))
|
|
946
|
+
|
|
947
|
+
return results
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def route_after_supervisor_agent(state: VibeSurfState) -> str:
|
|
951
|
+
"""Route based on supervisor agent decisions"""
|
|
952
|
+
if state.current_step == "browser_task_execution":
|
|
953
|
+
return "browser_task_execution"
|
|
954
|
+
elif state.current_step == "report_task_execution":
|
|
955
|
+
return "report_task_execution"
|
|
956
|
+
elif state.current_step == "summary_generation":
|
|
957
|
+
return "summary_generation" # Summary generated, go to END
|
|
958
|
+
elif state.current_step == "simple_response":
|
|
959
|
+
return "simple_response"
|
|
960
|
+
elif state.current_step == "supervisor_agent":
|
|
961
|
+
return "supervisor_agent" # Continue in supervisor loop
|
|
962
|
+
else:
|
|
963
|
+
return "END" # Default fallback - complete workflow
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def route_after_browser_task_execution(state: VibeSurfState) -> str:
|
|
967
|
+
"""Route back to supervisor after browser task completion"""
|
|
968
|
+
return "supervisor_agent"
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def route_after_report_task_execution(state: VibeSurfState) -> str:
|
|
972
|
+
"""Route back to supervisor after report task completion"""
|
|
973
|
+
return "supervisor_agent"
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def should_continue(state: VibeSurfState) -> str:
|
|
977
|
+
"""Main continuation logic"""
|
|
978
|
+
if state.is_complete:
|
|
979
|
+
return "END"
|
|
980
|
+
else:
|
|
981
|
+
return "continue"
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def create_vibe_surf_workflow() -> StateGraph:
|
|
985
|
+
"""Create the simplified LangGraph workflow with supervisor agent as core controller"""
|
|
986
|
+
|
|
987
|
+
workflow = StateGraph(VibeSurfState)
|
|
988
|
+
|
|
989
|
+
# Add nodes for simplified architecture
|
|
990
|
+
workflow.add_node("supervisor_agent", supervisor_agent_node)
|
|
991
|
+
workflow.add_node("browser_task_execution", browser_task_execution_node)
|
|
992
|
+
workflow.add_node("report_task_execution", report_task_execution_node)
|
|
993
|
+
|
|
994
|
+
# Set entry point
|
|
995
|
+
workflow.set_entry_point("supervisor_agent")
|
|
996
|
+
|
|
997
|
+
# Supervisor agent routes to different execution nodes or END
|
|
998
|
+
workflow.add_conditional_edges(
|
|
999
|
+
"supervisor_agent",
|
|
1000
|
+
route_after_supervisor_agent,
|
|
1001
|
+
{
|
|
1002
|
+
"browser_task_execution": "browser_task_execution",
|
|
1003
|
+
"report_task_execution": "report_task_execution",
|
|
1004
|
+
"summary_generation": END,
|
|
1005
|
+
"supervisor_agent": "supervisor_agent",
|
|
1006
|
+
"simple_response": END,
|
|
1007
|
+
"END": END
|
|
1008
|
+
}
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
# Execution nodes return to supervisor
|
|
1012
|
+
workflow.add_conditional_edges(
|
|
1013
|
+
"browser_task_execution",
|
|
1014
|
+
route_after_browser_task_execution,
|
|
1015
|
+
{
|
|
1016
|
+
"supervisor_agent": "supervisor_agent"
|
|
1017
|
+
}
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
workflow.add_conditional_edges(
|
|
1021
|
+
"report_task_execution",
|
|
1022
|
+
route_after_report_task_execution,
|
|
1023
|
+
{
|
|
1024
|
+
"supervisor_agent": "supervisor_agent"
|
|
1025
|
+
}
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
return workflow
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
class VibeSurfAgent:
|
|
1032
|
+
"""Main LangGraph-based VibeSurfAgent with comprehensive control capabilities"""
|
|
1033
|
+
|
|
1034
|
+
def __init__(
|
|
1035
|
+
self,
|
|
1036
|
+
llm: BaseChatModel,
|
|
1037
|
+
browser_manager: BrowserManager,
|
|
1038
|
+
controller: VibeSurfController,
|
|
1039
|
+
workspace_dir: str = "./workspace",
|
|
1040
|
+
):
|
|
1041
|
+
"""Initialize VibeSurfAgent with required components"""
|
|
1042
|
+
self.llm = llm
|
|
1043
|
+
self.browser_manager = browser_manager
|
|
1044
|
+
self.controller = controller
|
|
1045
|
+
self.workspace_dir = workspace_dir
|
|
1046
|
+
os.makedirs(self.workspace_dir, exist_ok=True)
|
|
1047
|
+
self.cur_session_id = None
|
|
1048
|
+
self.message_history = self.load_message_history()
|
|
1049
|
+
self.activity_logs = self.load_activity_logs()
|
|
1050
|
+
|
|
1051
|
+
# Create LangGraph workflow
|
|
1052
|
+
self.workflow = create_vibe_surf_workflow()
|
|
1053
|
+
self.app = self.workflow.compile()
|
|
1054
|
+
|
|
1055
|
+
# Control state management
|
|
1056
|
+
self._control_lock = asyncio.Lock()
|
|
1057
|
+
self._current_state: Optional[VibeSurfState] = None
|
|
1058
|
+
self._running_agents: Dict[str, Any] = {} # Track running BrowserUseAgent instances
|
|
1059
|
+
self._execution_task: Optional[asyncio.Task] = None
|
|
1060
|
+
|
|
1061
|
+
logger.info("🌊 VibeSurfAgent initialized with LangGraph workflow and control capabilities")
|
|
1062
|
+
|
|
1063
|
+
def load_message_history(self, message_history_path: Optional[str] = None):
|
|
1064
|
+
if message_history_path is None:
|
|
1065
|
+
message_history_path = os.path.join(self.workspace_dir, "message_history.pkl")
|
|
1066
|
+
if not os.path.exists(message_history_path):
|
|
1067
|
+
return defaultdict(list)
|
|
1068
|
+
with open(message_history_path, "rb") as f:
|
|
1069
|
+
message_history = pickle.load(f)
|
|
1070
|
+
logger.info(f"Loading message history from {message_history_path}")
|
|
1071
|
+
for session_id in message_history:
|
|
1072
|
+
logger.info(f"{session_id} has {len(message_history[session_id])} messages.")
|
|
1073
|
+
return message_history
|
|
1074
|
+
|
|
1075
|
+
def save_message_history(self, message_history_path: Optional[str] = None):
|
|
1076
|
+
if message_history_path is None:
|
|
1077
|
+
message_history_path = os.path.join(self.workspace_dir, "message_history.pkl")
|
|
1078
|
+
|
|
1079
|
+
with open(message_history_path, "wb") as f:
|
|
1080
|
+
logger.info(f"Saving message history with {len(self.message_history)} sessions to {message_history_path}")
|
|
1081
|
+
pickle.dump(self.message_history, f)
|
|
1082
|
+
|
|
1083
|
+
def load_activity_logs(self, activity_logs_path: Optional[str] = None):
|
|
1084
|
+
if activity_logs_path is None:
|
|
1085
|
+
activity_logs_path = os.path.join(self.workspace_dir, "activity_logs.pkl")
|
|
1086
|
+
if not os.path.exists(activity_logs_path):
|
|
1087
|
+
return defaultdict(list)
|
|
1088
|
+
with open(activity_logs_path, "rb") as f:
|
|
1089
|
+
activity_logs = pickle.load(f)
|
|
1090
|
+
logger.info(f"Loading activity logs from {activity_logs_path}")
|
|
1091
|
+
for session_id in activity_logs:
|
|
1092
|
+
logger.info(f"{session_id} has {len(activity_logs[session_id])} activity logs.")
|
|
1093
|
+
return activity_logs
|
|
1094
|
+
|
|
1095
|
+
def save_activity_logs(self, activity_logs_path: Optional[str] = None):
|
|
1096
|
+
if activity_logs_path is None:
|
|
1097
|
+
activity_logs_path = os.path.join(self.workspace_dir, "activity_logs.pkl")
|
|
1098
|
+
|
|
1099
|
+
with open(activity_logs_path, "wb") as f:
|
|
1100
|
+
logger.info(f"Saving activity logs with {len(self.activity_logs)} sessions to {activity_logs_path}")
|
|
1101
|
+
pickle.dump(self.activity_logs, f)
|
|
1102
|
+
|
|
1103
|
+
async def stop(self, reason: str = None) -> ControlResult:
|
|
1104
|
+
"""
|
|
1105
|
+
Stop the vibesurf execution immediately
|
|
1106
|
+
|
|
1107
|
+
Args:
|
|
1108
|
+
reason: Optional reason for stopping
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
ControlResult with operation status
|
|
1112
|
+
"""
|
|
1113
|
+
try:
|
|
1114
|
+
async with self._control_lock:
|
|
1115
|
+
reason = reason or "Manual stop requested"
|
|
1116
|
+
logger.info(f"🛑 Stopping agent execution: {reason}")
|
|
1117
|
+
|
|
1118
|
+
if self.cur_session_id in self.message_history:
|
|
1119
|
+
supervisor_message_history = self.message_history[self.cur_session_id]
|
|
1120
|
+
supervisor_message_history.append(UserMessage(
|
|
1121
|
+
content=f"🛑 Stopping agent execution: {reason}"))
|
|
1122
|
+
|
|
1123
|
+
if self._current_state:
|
|
1124
|
+
self._current_state.should_stop = True
|
|
1125
|
+
self._current_state.stopped = True
|
|
1126
|
+
self._current_state.control_timestamps["stopped"] = datetime.now()
|
|
1127
|
+
self._current_state.control_reasons["stopped"] = reason
|
|
1128
|
+
self._current_state.last_control_action = "stop"
|
|
1129
|
+
|
|
1130
|
+
# Stop all running agents with timeout
|
|
1131
|
+
try:
|
|
1132
|
+
await asyncio.wait_for(self._stop_all_agents(reason), timeout=3.0)
|
|
1133
|
+
except asyncio.TimeoutError:
|
|
1134
|
+
logger.warning("⚠️ Agent stopping timed out, continuing with task cancellation")
|
|
1135
|
+
|
|
1136
|
+
# Cancel execution task if running
|
|
1137
|
+
if self._execution_task and not self._execution_task.done():
|
|
1138
|
+
self._execution_task.cancel()
|
|
1139
|
+
try:
|
|
1140
|
+
await asyncio.wait_for(self._execution_task, timeout=2.0)
|
|
1141
|
+
except (asyncio.CancelledError, asyncio.TimeoutError):
|
|
1142
|
+
logger.debug("🛑 Execution task cancelled or timed out")
|
|
1143
|
+
|
|
1144
|
+
logger.info(f"✅ VibeSurf execution stopped: {reason}")
|
|
1145
|
+
return ControlResult(
|
|
1146
|
+
success=True,
|
|
1147
|
+
message=f"VibeSurf stopped successfully: {reason}",
|
|
1148
|
+
details={"reason": reason}
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
except asyncio.TimeoutError:
|
|
1152
|
+
error_msg = f"Stop operation timed out after 10 seconds"
|
|
1153
|
+
logger.error(error_msg)
|
|
1154
|
+
return ControlResult(
|
|
1155
|
+
success=False,
|
|
1156
|
+
message=error_msg,
|
|
1157
|
+
details={"timeout": True}
|
|
1158
|
+
)
|
|
1159
|
+
except Exception as e:
|
|
1160
|
+
error_msg = f"Failed to stop VibeSurf: {str(e)}"
|
|
1161
|
+
logger.error(error_msg)
|
|
1162
|
+
return ControlResult(
|
|
1163
|
+
success=False,
|
|
1164
|
+
message=error_msg,
|
|
1165
|
+
details={"error": str(e)}
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
async def pause(self, reason: str = None) -> ControlResult:
|
|
1169
|
+
"""
|
|
1170
|
+
Pause the VibeSurf execution
|
|
1171
|
+
|
|
1172
|
+
Args:
|
|
1173
|
+
reason: Optional reason for pausing
|
|
1174
|
+
|
|
1175
|
+
Returns:
|
|
1176
|
+
ControlResult with operation status
|
|
1177
|
+
"""
|
|
1178
|
+
async with self._control_lock:
|
|
1179
|
+
try:
|
|
1180
|
+
reason = reason or "Manual pause requested"
|
|
1181
|
+
logger.info(f"⏸️ Pausing agent execution: {reason}")
|
|
1182
|
+
|
|
1183
|
+
if self.cur_session_id in self.message_history:
|
|
1184
|
+
supervisor_message_history = self.message_history[self.cur_session_id]
|
|
1185
|
+
supervisor_message_history.append(UserMessage(
|
|
1186
|
+
content=f"⏸️ Pausing agent execution: {reason}"))
|
|
1187
|
+
|
|
1188
|
+
if self._current_state:
|
|
1189
|
+
self._current_state.should_pause = True
|
|
1190
|
+
self._current_state.control_timestamps["pause_requested"] = datetime.now()
|
|
1191
|
+
self._current_state.control_reasons["paused"] = reason
|
|
1192
|
+
self._current_state.last_control_action = "pause"
|
|
1193
|
+
|
|
1194
|
+
# Pause all running agents
|
|
1195
|
+
await self._pause_all_agents(reason)
|
|
1196
|
+
|
|
1197
|
+
logger.info(f"✅ VibeSurf execution paused: {reason}")
|
|
1198
|
+
return ControlResult(
|
|
1199
|
+
success=True,
|
|
1200
|
+
message=f"VibeSurf paused successfully: {reason}",
|
|
1201
|
+
details={"reason": reason}
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
except Exception as e:
|
|
1205
|
+
error_msg = f"Failed to pause VibeSurf: {str(e)}"
|
|
1206
|
+
logger.error(error_msg)
|
|
1207
|
+
return ControlResult(
|
|
1208
|
+
success=False,
|
|
1209
|
+
message=error_msg,
|
|
1210
|
+
details={"error": str(e)}
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
async def resume(self, reason: str = None) -> ControlResult:
|
|
1214
|
+
"""
|
|
1215
|
+
Resume the VibeSurf execution
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
reason: Optional reason for resuming
|
|
1219
|
+
|
|
1220
|
+
Returns:
|
|
1221
|
+
ControlResult with operation status
|
|
1222
|
+
"""
|
|
1223
|
+
async with self._control_lock:
|
|
1224
|
+
try:
|
|
1225
|
+
reason = reason or "Manual resume requested"
|
|
1226
|
+
logger.info(f"▶️ Resuming agent execution: {reason}")
|
|
1227
|
+
|
|
1228
|
+
if self.cur_session_id in self.message_history:
|
|
1229
|
+
supervisor_message_history = self.message_history[self.cur_session_id]
|
|
1230
|
+
supervisor_message_history.append(UserMessage(
|
|
1231
|
+
content=f"▶️ Resuming agent execution: {reason}"))
|
|
1232
|
+
|
|
1233
|
+
if self._current_state:
|
|
1234
|
+
self._current_state.paused = False
|
|
1235
|
+
self._current_state.should_pause = False
|
|
1236
|
+
self._current_state.control_timestamps["resumed"] = datetime.now()
|
|
1237
|
+
self._current_state.control_reasons["resumed"] = reason
|
|
1238
|
+
self._current_state.last_control_action = "resume"
|
|
1239
|
+
|
|
1240
|
+
# Resume all paused agents
|
|
1241
|
+
await self._resume_all_agents(reason)
|
|
1242
|
+
|
|
1243
|
+
logger.info(f"✅ VibeSurf execution resumed: {reason}")
|
|
1244
|
+
return ControlResult(
|
|
1245
|
+
success=True,
|
|
1246
|
+
message=f"VibeSurf resumed successfully: {reason}",
|
|
1247
|
+
details={"reason": reason}
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
except Exception as e:
|
|
1251
|
+
error_msg = f"Failed to resume VibeSurf: {str(e)}"
|
|
1252
|
+
logger.error(error_msg)
|
|
1253
|
+
return ControlResult(
|
|
1254
|
+
success=False,
|
|
1255
|
+
message=error_msg,
|
|
1256
|
+
details={"error": str(e)}
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
async def pause_agent(self, agent_id: str, reason: str = None) -> ControlResult:
|
|
1260
|
+
"""
|
|
1261
|
+
Pause a specific agent
|
|
1262
|
+
|
|
1263
|
+
Args:
|
|
1264
|
+
agent_id: ID of the agent to pause
|
|
1265
|
+
reason: Optional reason for pausing
|
|
1266
|
+
|
|
1267
|
+
Returns:
|
|
1268
|
+
ControlResult with operation status
|
|
1269
|
+
"""
|
|
1270
|
+
async with self._control_lock:
|
|
1271
|
+
try:
|
|
1272
|
+
reason = reason or f"Manual pause requested for agent {agent_id}"
|
|
1273
|
+
logger.info(f"⏸️ Pausing agent {agent_id}: {reason}")
|
|
1274
|
+
|
|
1275
|
+
# Update state tracking
|
|
1276
|
+
if self._current_state:
|
|
1277
|
+
self._current_state.paused_agents.add(agent_id)
|
|
1278
|
+
if agent_id not in self._current_state.agent_control_states:
|
|
1279
|
+
self._current_state.agent_control_states[agent_id] = {}
|
|
1280
|
+
self._current_state.agent_control_states[agent_id]["paused"] = True
|
|
1281
|
+
self._current_state.agent_control_states[agent_id]["pause_reason"] = reason
|
|
1282
|
+
self._current_state.agent_control_states[agent_id]["pause_timestamp"] = datetime.now()
|
|
1283
|
+
|
|
1284
|
+
# Pause the specific agent if it's running
|
|
1285
|
+
agent = self._running_agents.get(agent_id)
|
|
1286
|
+
if agent:
|
|
1287
|
+
if hasattr(agent, 'pause'):
|
|
1288
|
+
await agent.pause()
|
|
1289
|
+
logger.info(f"✅ Agent {agent_id} paused successfully")
|
|
1290
|
+
else:
|
|
1291
|
+
logger.warning(f"⚠️ Agent {agent_id} not found")
|
|
1292
|
+
|
|
1293
|
+
return ControlResult(
|
|
1294
|
+
success=True,
|
|
1295
|
+
message=f"Agent {agent_id} paused successfully: {reason}",
|
|
1296
|
+
details={"agent_id": agent_id, "reason": reason}
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
except Exception as e:
|
|
1300
|
+
error_msg = f"Failed to pause agent {agent_id}: {str(e)}"
|
|
1301
|
+
logger.error(error_msg)
|
|
1302
|
+
return ControlResult(
|
|
1303
|
+
success=False,
|
|
1304
|
+
message=error_msg,
|
|
1305
|
+
details={"agent_id": agent_id, "error": str(e)}
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
async def resume_agent(self, agent_id: str, reason: str = None) -> ControlResult:
|
|
1309
|
+
"""
|
|
1310
|
+
Resume a specific agent
|
|
1311
|
+
|
|
1312
|
+
Args:
|
|
1313
|
+
agent_id: ID of the agent to resume
|
|
1314
|
+
reason: Optional reason for resuming
|
|
1315
|
+
|
|
1316
|
+
Returns:
|
|
1317
|
+
ControlResult with operation status
|
|
1318
|
+
"""
|
|
1319
|
+
async with self._control_lock:
|
|
1320
|
+
try:
|
|
1321
|
+
reason = reason or f"Manual resume requested for agent {agent_id}"
|
|
1322
|
+
logger.info(f"▶️ Resuming agent {agent_id}: {reason}")
|
|
1323
|
+
|
|
1324
|
+
# Update state tracking
|
|
1325
|
+
if self._current_state:
|
|
1326
|
+
self._current_state.paused_agents.discard(agent_id)
|
|
1327
|
+
if agent_id not in self._current_state.agent_control_states:
|
|
1328
|
+
self._current_state.agent_control_states[agent_id] = {}
|
|
1329
|
+
self._current_state.agent_control_states[agent_id]["paused"] = False
|
|
1330
|
+
self._current_state.agent_control_states[agent_id]["resume_reason"] = reason
|
|
1331
|
+
self._current_state.agent_control_states[agent_id]["resume_timestamp"] = datetime.now()
|
|
1332
|
+
|
|
1333
|
+
# Resume the specific agent if it's running
|
|
1334
|
+
agent = self._running_agents.get(agent_id)
|
|
1335
|
+
if agent:
|
|
1336
|
+
if hasattr(agent, 'resume'):
|
|
1337
|
+
await agent.resume()
|
|
1338
|
+
logger.info(f"✅ Agent {agent_id} resumed successfully")
|
|
1339
|
+
else:
|
|
1340
|
+
logger.warning(f"⚠️ Agent {agent_id} not found")
|
|
1341
|
+
|
|
1342
|
+
return ControlResult(
|
|
1343
|
+
success=True,
|
|
1344
|
+
message=f"Agent {agent_id} resumed successfully: {reason}",
|
|
1345
|
+
details={"agent_id": agent_id, "reason": reason}
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
except Exception as e:
|
|
1349
|
+
error_msg = f"Failed to resume agent {agent_id}: {str(e)}"
|
|
1350
|
+
logger.error(error_msg)
|
|
1351
|
+
return ControlResult(
|
|
1352
|
+
success=False,
|
|
1353
|
+
message=error_msg,
|
|
1354
|
+
details={"agent_id": agent_id, "error": str(e)}
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
def get_status(self) -> VibeSurfStatus:
|
|
1358
|
+
"""
|
|
1359
|
+
Get current status of the VibeSurf and all agents
|
|
1360
|
+
|
|
1361
|
+
Returns:
|
|
1362
|
+
VibeSurfStatus with current state information
|
|
1363
|
+
"""
|
|
1364
|
+
try:
|
|
1365
|
+
# Determine overall status
|
|
1366
|
+
if not self._current_state:
|
|
1367
|
+
overall_status = "idle"
|
|
1368
|
+
elif self._current_state.stopped:
|
|
1369
|
+
overall_status = "stopped"
|
|
1370
|
+
elif self._current_state.paused or self._current_state.should_pause:
|
|
1371
|
+
overall_status = "paused"
|
|
1372
|
+
elif self._current_state.is_complete:
|
|
1373
|
+
overall_status = "idle"
|
|
1374
|
+
else:
|
|
1375
|
+
overall_status = "running"
|
|
1376
|
+
|
|
1377
|
+
# Build agent statuses
|
|
1378
|
+
agent_statuses = {}
|
|
1379
|
+
|
|
1380
|
+
# Add status for tracked running agents
|
|
1381
|
+
for agent_id, agent in self._running_agents.items():
|
|
1382
|
+
status = "running"
|
|
1383
|
+
current_action = None
|
|
1384
|
+
error_message = None
|
|
1385
|
+
pause_reason = None
|
|
1386
|
+
|
|
1387
|
+
# Check if agent is paused
|
|
1388
|
+
if self._current_state and agent_id in self._current_state.paused_agents:
|
|
1389
|
+
status = "paused"
|
|
1390
|
+
agent_state = self._current_state.agent_control_states.get(agent_id, {})
|
|
1391
|
+
pause_reason = agent_state.get("pause_reason")
|
|
1392
|
+
elif self._current_state and self._current_state.stopped:
|
|
1393
|
+
status = "stopped"
|
|
1394
|
+
|
|
1395
|
+
# Get current action if available
|
|
1396
|
+
if agent and hasattr(agent, 'state'):
|
|
1397
|
+
try:
|
|
1398
|
+
if hasattr(agent.state, 'last_result') and agent.state.last_result:
|
|
1399
|
+
current_action = f"Last action completed"
|
|
1400
|
+
else:
|
|
1401
|
+
current_action = "Executing task"
|
|
1402
|
+
except:
|
|
1403
|
+
current_action = "Unknown"
|
|
1404
|
+
|
|
1405
|
+
agent_statuses[agent_id] = AgentStatus(
|
|
1406
|
+
agent_id=agent_id,
|
|
1407
|
+
status=status,
|
|
1408
|
+
current_action=current_action,
|
|
1409
|
+
error_message=error_message,
|
|
1410
|
+
pause_reason=pause_reason
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
# Build progress information
|
|
1414
|
+
progress = {}
|
|
1415
|
+
if self._current_state:
|
|
1416
|
+
progress = {
|
|
1417
|
+
"current_step": self._current_state.current_step,
|
|
1418
|
+
"completed_todos": len(self._current_state.completed_todos),
|
|
1419
|
+
"total_todos": len(self._current_state.todo_list),
|
|
1420
|
+
"current_task_index": self._current_state.current_task_index,
|
|
1421
|
+
"is_complete": self._current_state.is_complete,
|
|
1422
|
+
"last_control_action": self._current_state.last_control_action
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return VibeSurfStatus(
|
|
1426
|
+
overall_status=overall_status,
|
|
1427
|
+
agent_statuses=agent_statuses,
|
|
1428
|
+
progress=progress,
|
|
1429
|
+
active_step=self._current_state.current_step if self._current_state else None
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
except Exception as e:
|
|
1433
|
+
logger.error(f"❌ Failed to get status: {e}")
|
|
1434
|
+
return VibeSurfStatus(
|
|
1435
|
+
overall_status="error",
|
|
1436
|
+
agent_statuses={},
|
|
1437
|
+
progress={"error": str(e)}
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
async def _stop_all_agents(self, reason: str) -> None:
|
|
1441
|
+
"""Stop all running agents"""
|
|
1442
|
+
for agent_id, agent in self._running_agents.items():
|
|
1443
|
+
try:
|
|
1444
|
+
# Also try to pause if available as a fallback
|
|
1445
|
+
if agent and hasattr(agent, 'stop'):
|
|
1446
|
+
await agent.stop()
|
|
1447
|
+
logger.debug(f"⏸️ stop agent {agent_id}")
|
|
1448
|
+
except Exception as e:
|
|
1449
|
+
logger.warning(f"⚠️ Failed to stop agent {agent_id}: {e}")
|
|
1450
|
+
|
|
1451
|
+
async def _pause_all_agents(self, reason: str) -> None:
|
|
1452
|
+
"""Pause all running agents"""
|
|
1453
|
+
for agent_id, agent in self._running_agents.items():
|
|
1454
|
+
try:
|
|
1455
|
+
if hasattr(agent, 'pause'):
|
|
1456
|
+
await agent.pause()
|
|
1457
|
+
logger.debug(f"⏸️ Paused agent {agent_id}")
|
|
1458
|
+
if self._current_state:
|
|
1459
|
+
self._current_state.paused_agents.add(agent_id)
|
|
1460
|
+
except Exception as e:
|
|
1461
|
+
logger.warning(f"⚠️ Failed to pause agent {agent_id}: {e}")
|
|
1462
|
+
|
|
1463
|
+
async def _resume_all_agents(self, reason: str) -> None:
|
|
1464
|
+
"""Resume all paused agents"""
|
|
1465
|
+
for agent_id, agent in self._running_agents.items():
|
|
1466
|
+
try:
|
|
1467
|
+
if hasattr(agent, 'resume'):
|
|
1468
|
+
await agent.resume()
|
|
1469
|
+
logger.debug(f"▶️ Resumed agent {agent_id}")
|
|
1470
|
+
if self._current_state:
|
|
1471
|
+
self._current_state.paused_agents.discard(agent_id)
|
|
1472
|
+
except Exception as e:
|
|
1473
|
+
logger.warning(f"⚠️ Failed to resume agent {agent_id}: {e}")
|
|
1474
|
+
|
|
1475
|
+
async def run(
|
|
1476
|
+
self,
|
|
1477
|
+
task: str,
|
|
1478
|
+
upload_files: Optional[List[str]] = None,
|
|
1479
|
+
session_id: Optional[str] = None,
|
|
1480
|
+
) -> str:
|
|
1481
|
+
"""
|
|
1482
|
+
Main execution method that returns markdown summary with control capabilities
|
|
1483
|
+
|
|
1484
|
+
Args:
|
|
1485
|
+
task: User task to execute
|
|
1486
|
+
upload_files: Optional list of file paths that user has uploaded
|
|
1487
|
+
|
|
1488
|
+
Returns:
|
|
1489
|
+
str: Markdown summary of execution results
|
|
1490
|
+
"""
|
|
1491
|
+
logger.info(f"🚀 Starting VibeSurfAgent execution for task: {task[:100]}...")
|
|
1492
|
+
agent_activity_logs = None
|
|
1493
|
+
try:
|
|
1494
|
+
session_id = session_id or self.cur_session_id or uuid7str()
|
|
1495
|
+
if session_id != self.cur_session_id:
|
|
1496
|
+
self.cur_session_id = session_id
|
|
1497
|
+
|
|
1498
|
+
if self.cur_session_id not in self.message_history:
|
|
1499
|
+
logger.info(f"{self.cur_session_id} not found in message_history, create a new one.")
|
|
1500
|
+
self.message_history[self.cur_session_id] = []
|
|
1501
|
+
supervisor_message_history = self.message_history[self.cur_session_id]
|
|
1502
|
+
if not supervisor_message_history:
|
|
1503
|
+
supervisor_message_history.append(SystemMessage(content=SUPERVISOR_AGENT_SYSTEM_PROMPT))
|
|
1504
|
+
if upload_files and not isinstance(upload_files, list):
|
|
1505
|
+
upload_files = [upload_files]
|
|
1506
|
+
upload_files_md = format_upload_files_list(upload_files)
|
|
1507
|
+
supervisor_message_history.append(
|
|
1508
|
+
UserMessage(
|
|
1509
|
+
content=f"* User's New Request:\n{task}\n* Uploaded Files for Completing Task:\n{upload_files_md}\n")
|
|
1510
|
+
)
|
|
1511
|
+
logger.info(f"* User's New Request:\n{task}\n* Uploaded Files for Completing Task:\n{upload_files_md}\n")
|
|
1512
|
+
|
|
1513
|
+
if self.cur_session_id not in self.activity_logs:
|
|
1514
|
+
self.activity_logs[self.cur_session_id] = []
|
|
1515
|
+
agent_activity_logs = self.activity_logs[self.cur_session_id]
|
|
1516
|
+
activity_entry = {
|
|
1517
|
+
"agent_name": 'user',
|
|
1518
|
+
"agent_status": 'request', # working, result, error
|
|
1519
|
+
"agent_msg": f"{task}\nUpload Files:\n{upload_files_md}\n" if upload_files else f"{task}"
|
|
1520
|
+
}
|
|
1521
|
+
agent_activity_logs.append(activity_entry)
|
|
1522
|
+
|
|
1523
|
+
# Initialize state
|
|
1524
|
+
initial_state = VibeSurfState(
|
|
1525
|
+
original_task=task,
|
|
1526
|
+
upload_files=upload_files or [],
|
|
1527
|
+
workspace_dir=self.workspace_dir,
|
|
1528
|
+
browser_manager=self.browser_manager,
|
|
1529
|
+
vibesurf_controller=self.controller,
|
|
1530
|
+
agent_activity_logs=agent_activity_logs,
|
|
1531
|
+
supervisor_message_history=supervisor_message_history,
|
|
1532
|
+
llm=self.llm,
|
|
1533
|
+
session_id=session_id,
|
|
1534
|
+
vibesurf_agent=self # Reference to VibeSurfAgent for control coordination
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
# Set current state for control operations
|
|
1538
|
+
async with self._control_lock:
|
|
1539
|
+
self._current_state = initial_state
|
|
1540
|
+
self._running_agents.clear() # Clear any previous agents
|
|
1541
|
+
|
|
1542
|
+
async def _execute_workflow():
|
|
1543
|
+
"""Internal workflow execution with proper state management"""
|
|
1544
|
+
try:
|
|
1545
|
+
# Run without checkpoints
|
|
1546
|
+
logger.info("🔄 Executing LangGraph workflow...")
|
|
1547
|
+
return await self.app.ainvoke(initial_state)
|
|
1548
|
+
finally:
|
|
1549
|
+
# Clean up running agents
|
|
1550
|
+
async with self._control_lock:
|
|
1551
|
+
self._running_agents.clear()
|
|
1552
|
+
|
|
1553
|
+
# Execute workflow as a task for control management
|
|
1554
|
+
self._execution_task = asyncio.create_task(_execute_workflow())
|
|
1555
|
+
final_state = await self._execution_task
|
|
1556
|
+
|
|
1557
|
+
# Update current state reference
|
|
1558
|
+
async with self._control_lock:
|
|
1559
|
+
self._current_state = final_state
|
|
1560
|
+
|
|
1561
|
+
# Get final result
|
|
1562
|
+
result = await self._get_result(final_state)
|
|
1563
|
+
logger.info("✅ VibeSurfAgent execution completed")
|
|
1564
|
+
return result
|
|
1565
|
+
|
|
1566
|
+
except asyncio.CancelledError:
|
|
1567
|
+
logger.info("🛑 VibeSurfAgent execution was cancelled")
|
|
1568
|
+
return f"# Task Execution Cancelled\n\n**Task:** {task}\n\nExecution was stopped by user request."
|
|
1569
|
+
except Exception as e:
|
|
1570
|
+
logger.error(f"❌ VibeSurfAgent execution failed: {e}")
|
|
1571
|
+
return f"# Task Execution Failed\n\n**Task:** {task}\n\n**Error:** {str(e)}\n\nPlease try again or contact support."
|
|
1572
|
+
finally:
|
|
1573
|
+
# Reset state
|
|
1574
|
+
self.save_message_history()
|
|
1575
|
+
self.save_activity_logs()
|
|
1576
|
+
if agent_activity_logs:
|
|
1577
|
+
activity_entry = {
|
|
1578
|
+
"agent_name": "VibeSurfAgent",
|
|
1579
|
+
"agent_status": "done", # working, result, error
|
|
1580
|
+
"agent_msg": "Finish Task."
|
|
1581
|
+
}
|
|
1582
|
+
agent_activity_logs.append(activity_entry)
|
|
1583
|
+
async with self._control_lock:
|
|
1584
|
+
self._current_state = None
|
|
1585
|
+
self._execution_task = None
|
|
1586
|
+
self._running_agents.clear()
|
|
1587
|
+
|
|
1588
|
+
def get_activity_logs(self, session_id: Optional[str] = None, message_index: Optional[int] = None) -> Optional[
|
|
1589
|
+
List[Dict]]:
|
|
1590
|
+
if session_id is None:
|
|
1591
|
+
session_id = self.cur_session_id
|
|
1592
|
+
|
|
1593
|
+
logger.info(f"📊 GET_ACTIVITY_LOGS DEBUG - Session: {session_id}, Message Index: {message_index}, Current Session: {self.cur_session_id}")
|
|
1594
|
+
|
|
1595
|
+
# Ensure session_id exists in activity_logs
|
|
1596
|
+
if session_id not in self.activity_logs:
|
|
1597
|
+
logger.warning(f"⚠️ Session {session_id} not found in activity_logs. Available sessions: {list(self.activity_logs.keys())}")
|
|
1598
|
+
return None
|
|
1599
|
+
|
|
1600
|
+
session_logs = self.activity_logs[session_id]
|
|
1601
|
+
logger.info(f"📋 Session {session_id} has {len(session_logs)} activity logs")
|
|
1602
|
+
|
|
1603
|
+
if message_index is None:
|
|
1604
|
+
logger.info(f"📤 Returning all {len(session_logs)} activity logs for session {session_id}")
|
|
1605
|
+
return session_logs
|
|
1606
|
+
else:
|
|
1607
|
+
if message_index >= len(session_logs):
|
|
1608
|
+
logger.debug(f"⚠️ Message index {message_index} out of range for session {session_id} (max index: {len(session_logs) - 1})")
|
|
1609
|
+
return None
|
|
1610
|
+
else:
|
|
1611
|
+
activity_log = session_logs[message_index]
|
|
1612
|
+
logger.info(f"📤 Returning activity log at index {message_index}: {activity_log.get('agent_name', 'unknown')} - {activity_log.get('agent_status', 'unknown')}")
|
|
1613
|
+
return activity_log
|
|
1614
|
+
|
|
1615
|
+
async def _get_result(self, state) -> str:
|
|
1616
|
+
"""Get the final result from execution with simplified workflow support"""
|
|
1617
|
+
# Handle both dict and dataclass state types due to LangGraph serialization
|
|
1618
|
+
simple_response = state.get('simple_response') if isinstance(state, dict) else getattr(state, 'simple_response',
|
|
1619
|
+
None)
|
|
1620
|
+
final_summary = state.get('final_summary') if isinstance(state, dict) else getattr(state, 'final_summary', None)
|
|
1621
|
+
original_task = state.get('original_task') if isinstance(state, dict) else getattr(state, 'original_task',
|
|
1622
|
+
'Unknown task')
|
|
1623
|
+
|
|
1624
|
+
# Fallback for cases where state doesn't support logging
|
|
1625
|
+
if simple_response:
|
|
1626
|
+
return f"# Response\n\n{simple_response}"
|
|
1627
|
+
elif final_summary:
|
|
1628
|
+
return final_summary
|
|
1629
|
+
else:
|
|
1630
|
+
return f"# Task Execution Completed\n\n**Task:** {original_task}\n\nTask execution completed but no detailed result available."
|
|
1631
|
+
|
|
1632
|
+
workflow = create_vibe_surf_workflow()
|