vibesurf 0.1.10__py3-none-any.whl → 0.1.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of vibesurf might be problematic. Click here for more details.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +68 -45
- vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
- vibe_surf/agents/report_writer_agent.py +380 -226
- vibe_surf/agents/vibe_surf_agent.py +879 -825
- vibe_surf/agents/views.py +130 -0
- vibe_surf/backend/api/activity.py +3 -1
- vibe_surf/backend/api/browser.py +9 -5
- vibe_surf/backend/api/config.py +8 -5
- vibe_surf/backend/api/files.py +59 -50
- vibe_surf/backend/api/models.py +2 -2
- vibe_surf/backend/api/task.py +45 -12
- vibe_surf/backend/database/manager.py +24 -18
- vibe_surf/backend/database/queries.py +199 -192
- vibe_surf/backend/database/schemas.py +1 -1
- vibe_surf/backend/main.py +4 -2
- vibe_surf/backend/shared_state.py +28 -35
- vibe_surf/backend/utils/encryption.py +3 -1
- vibe_surf/backend/utils/llm_factory.py +41 -36
- vibe_surf/browser/agent_browser_session.py +0 -4
- vibe_surf/browser/browser_manager.py +14 -8
- vibe_surf/browser/utils.py +5 -3
- vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
- vibe_surf/chrome_extension/background.js +4 -0
- vibe_surf/chrome_extension/scripts/api-client.js +13 -0
- vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
- vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
- vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
- vibe_surf/chrome_extension/sidepanel.html +21 -4
- vibe_surf/chrome_extension/styles/activity.css +365 -5
- vibe_surf/chrome_extension/styles/input.css +139 -0
- vibe_surf/cli.py +4 -22
- vibe_surf/common.py +35 -0
- vibe_surf/llm/openai_compatible.py +148 -93
- vibe_surf/logger.py +99 -0
- vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
- vibe_surf/tools/file_system.py +415 -0
- vibe_surf/{controller → tools}/mcp_client.py +4 -3
- vibe_surf/tools/report_writer_tools.py +21 -0
- vibe_surf/tools/vibesurf_tools.py +657 -0
- vibe_surf/tools/views.py +120 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/METADATA +6 -2
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/RECORD +49 -43
- vibe_surf/controller/file_system.py +0 -53
- vibe_surf/controller/views.py +0 -37
- /vibe_surf/{controller → tools}/__init__.py +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
from typing import Any, Generic, TypeVar
|
|
2
|
+
from browser_use.tools.registry.service import Registry
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
import pdb
|
|
5
|
+
import os
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import enum
|
|
9
|
+
import base64
|
|
10
|
+
import mimetypes
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Optional, Type, Callable, Dict, Any, Union, Awaitable, TypeVar
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from browser_use.tools.service import Controller, Tools, handle_browser_error
|
|
15
|
+
import logging
|
|
16
|
+
from browser_use.agent.views import ActionModel, ActionResult
|
|
17
|
+
from browser_use.utils import time_execution_sync
|
|
18
|
+
from browser_use.filesystem.file_system import FileSystem
|
|
19
|
+
from browser_use.browser import BrowserSession
|
|
20
|
+
from browser_use.llm.base import BaseChatModel
|
|
21
|
+
from browser_use.llm.messages import UserMessage, ContentPartTextParam, ContentPartImageParam, ImageURL
|
|
22
|
+
from browser_use.dom.service import EnhancedDOMTreeNode
|
|
23
|
+
from browser_use.browser.views import BrowserError
|
|
24
|
+
from browser_use.mcp.client import MCPClient
|
|
25
|
+
|
|
26
|
+
from vibe_surf.browser.agent_browser_session import AgentBrowserSession
|
|
27
|
+
from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction, BrowserUseAgentExecution, \
|
|
28
|
+
ReportWriterTask, TodoGenerateAction, TodoModifyAction, VibeSurfDoneAction
|
|
29
|
+
from vibe_surf.tools.mcp_client import CustomMCPClient
|
|
30
|
+
from vibe_surf.tools.file_system import CustomFileSystem
|
|
31
|
+
from vibe_surf.browser.browser_manager import BrowserManager
|
|
32
|
+
|
|
33
|
+
from vibe_surf.logger import get_logger
|
|
34
|
+
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
Context = TypeVar('Context')
|
|
38
|
+
|
|
39
|
+
T = TypeVar('T', bound=BaseModel)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class VibeSurfTools:
|
|
43
|
+
def __init__(self, exclude_actions: list[str] = [], mcp_server_config: Optional[Dict[str, Any]] = None):
|
|
44
|
+
self.registry = Registry(exclude_actions)
|
|
45
|
+
self._register_file_actions()
|
|
46
|
+
self._register_browser_use_agent()
|
|
47
|
+
self._register_report_writer_agent()
|
|
48
|
+
self._register_todo_actions()
|
|
49
|
+
self._register_done_action()
|
|
50
|
+
self.mcp_server_config = mcp_server_config
|
|
51
|
+
self.mcp_clients: Dict[str, MCPClient] = {}
|
|
52
|
+
|
|
53
|
+
def _register_browser_use_agent(self):
|
|
54
|
+
@self.registry.action(
|
|
55
|
+
'Execute browser_use agent tasks. Supports both single task execution (list length=1) and '
|
|
56
|
+
'parallel execution of multiple tasks for improved efficiency. '
|
|
57
|
+
'Accepts a list of tasks where each task can specify a tab_id (optional), '
|
|
58
|
+
'task description (focusing on goals and expected returns), and task_files (optional). '
|
|
59
|
+
'Browser_use agent has strong planning and execution capabilities, only needs task descriptions and desired outcomes.',
|
|
60
|
+
param_model=BrowserUseAgentExecution,
|
|
61
|
+
)
|
|
62
|
+
async def execute_browser_use_agent(
|
|
63
|
+
params: BrowserUseAgentExecution,
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Execute browser_use agent tasks in parallel for improved efficiency.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
params: BrowserUseAgentExecution containing list of tasks to execute
|
|
70
|
+
browser_manager: Browser manager instance
|
|
71
|
+
llm: Language model instance
|
|
72
|
+
file_system: File system instance
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
ActionResult with execution results
|
|
76
|
+
"""
|
|
77
|
+
# TODO: Implement parallel execution of browser_use agent tasks
|
|
78
|
+
# This is a placeholder implementation
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
def _register_report_writer_agent(self):
|
|
82
|
+
@self.registry.action(
|
|
83
|
+
'Execute report writer agent to generate HTML reports. '
|
|
84
|
+
'Task should describe report requirements, goals, insights observed, and any hints or tips for generating the report.',
|
|
85
|
+
param_model=ReportWriterTask,
|
|
86
|
+
)
|
|
87
|
+
async def execute_report_writer_agent(
|
|
88
|
+
params: ReportWriterTask,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Execute report writer agent to generate HTML reports.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
params: ReportWriterTask containing task description with requirements and insights
|
|
95
|
+
browser_manager: Browser manager instance
|
|
96
|
+
llm: Language model instance
|
|
97
|
+
file_system: File system instance
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
ActionResult with generated report path
|
|
101
|
+
"""
|
|
102
|
+
# TODO: Implement report writer agent execution
|
|
103
|
+
# This is a placeholder implementation
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
def _register_todo_actions(self):
|
|
107
|
+
@self.registry.action(
|
|
108
|
+
'Generate a new todo.md file with the provided todo items in markdown checkbox format.'
|
|
109
|
+
)
|
|
110
|
+
async def generate_todos(todo_items: list[str], file_system: CustomFileSystem):
|
|
111
|
+
"""Generate a new todo.md file with todo items in markdown format"""
|
|
112
|
+
try:
|
|
113
|
+
# Format todo items as markdown checkboxes
|
|
114
|
+
formatted_items = []
|
|
115
|
+
for item in todo_items:
|
|
116
|
+
# Clean item and ensure it doesn't already have checkbox format
|
|
117
|
+
clean_item = item.strip()
|
|
118
|
+
if clean_item.startswith('- ['):
|
|
119
|
+
formatted_items.append(clean_item)
|
|
120
|
+
else:
|
|
121
|
+
formatted_items.append(f'- [ ] {clean_item}')
|
|
122
|
+
|
|
123
|
+
# Create content for todo.md
|
|
124
|
+
content = '\n'.join(formatted_items) + '\n'
|
|
125
|
+
|
|
126
|
+
# Write to todo.md file
|
|
127
|
+
todo_path = file_system.get_dir() / 'todo.md'
|
|
128
|
+
if todo_path.exists():
|
|
129
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
130
|
+
await file_system.move_file('todo.md', f'todos/todo-{timestamp}.md')
|
|
131
|
+
result = await file_system.write_file('todo.md', content)
|
|
132
|
+
|
|
133
|
+
logger.info(f'📝 Generated todo.md with {len(todo_items)} items')
|
|
134
|
+
return ActionResult(
|
|
135
|
+
extracted_content=f'Todo file generated successfully with {len(todo_items)} items:\n{content}',
|
|
136
|
+
long_term_memory=f'Generated todo.md with {len(todo_items)} items',
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f'❌ Failed to generate todo file: {e}')
|
|
141
|
+
raise RuntimeError(f'Failed to generate todo file: {str(e)}')
|
|
142
|
+
|
|
143
|
+
@self.registry.action(
|
|
144
|
+
'Read the current todo.md file content.'
|
|
145
|
+
)
|
|
146
|
+
async def read_todos(file_system: CustomFileSystem):
|
|
147
|
+
"""Read the current todo.md file content"""
|
|
148
|
+
try:
|
|
149
|
+
# Read todo.md file
|
|
150
|
+
result = await file_system.read_file('todo.md')
|
|
151
|
+
|
|
152
|
+
logger.info(f'📖 Read todo.md file')
|
|
153
|
+
return ActionResult(
|
|
154
|
+
extracted_content=result,
|
|
155
|
+
long_term_memory='Read current todo list',
|
|
156
|
+
include_in_memory=True,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f'❌ Failed to read todo file: {e}')
|
|
161
|
+
return ActionResult(
|
|
162
|
+
extracted_content='Error: todo.md file not found or could not be read',
|
|
163
|
+
long_term_memory='Failed to read todo file',
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@self.registry.action(
|
|
167
|
+
'Modify existing todo items in todo.md file. Supports add, remove, complete, and uncomplete operations.',
|
|
168
|
+
param_model=TodoModifyAction,
|
|
169
|
+
)
|
|
170
|
+
async def modify_todos(params: TodoModifyAction, file_system: CustomFileSystem):
|
|
171
|
+
"""Modify existing todo items using various operations"""
|
|
172
|
+
try:
|
|
173
|
+
# First read current content
|
|
174
|
+
current_content = await file_system.read_file('todo.md')
|
|
175
|
+
|
|
176
|
+
# Extract just the content part (remove the "Read from file..." prefix)
|
|
177
|
+
if '<content>' in current_content and '</content>' in current_content:
|
|
178
|
+
start = current_content.find('<content>') + len('<content>')
|
|
179
|
+
end = current_content.find('</content>')
|
|
180
|
+
content = current_content[start:end].strip()
|
|
181
|
+
else:
|
|
182
|
+
content = current_content.strip()
|
|
183
|
+
|
|
184
|
+
modified_content = content
|
|
185
|
+
changes_made = []
|
|
186
|
+
|
|
187
|
+
# Process each modification
|
|
188
|
+
for modification in params.modifications:
|
|
189
|
+
action = modification.action
|
|
190
|
+
item = modification.item.strip()
|
|
191
|
+
|
|
192
|
+
if action == 'add':
|
|
193
|
+
# Add new item
|
|
194
|
+
if item:
|
|
195
|
+
# Format as checkbox if not already formatted
|
|
196
|
+
if not item.startswith('- ['):
|
|
197
|
+
item = f'- [ ] {item}'
|
|
198
|
+
modified_content += f'\n{item}'
|
|
199
|
+
changes_made.append(f'Added: {item}')
|
|
200
|
+
|
|
201
|
+
elif action == 'remove':
|
|
202
|
+
# Remove item
|
|
203
|
+
if item:
|
|
204
|
+
# Try to find and remove the item (with some flexibility)
|
|
205
|
+
lines = modified_content.split('\n')
|
|
206
|
+
new_lines = []
|
|
207
|
+
removed = False
|
|
208
|
+
for line in lines:
|
|
209
|
+
if item in line or line.strip().endswith(item):
|
|
210
|
+
removed = True
|
|
211
|
+
changes_made.append(f'Removed: {line.strip()}')
|
|
212
|
+
else:
|
|
213
|
+
new_lines.append(line)
|
|
214
|
+
modified_content = '\n'.join(new_lines)
|
|
215
|
+
if not removed:
|
|
216
|
+
changes_made.append(f'Item not found for removal: {item}')
|
|
217
|
+
|
|
218
|
+
elif action == 'complete':
|
|
219
|
+
# Mark item as complete: - [ ] → - [x]
|
|
220
|
+
if item:
|
|
221
|
+
lines = modified_content.split('\n')
|
|
222
|
+
completed = False
|
|
223
|
+
for i, line in enumerate(lines):
|
|
224
|
+
if item in line and '- [ ]' in line:
|
|
225
|
+
lines[i] = line.replace('- [ ]', '- [x]')
|
|
226
|
+
completed = True
|
|
227
|
+
changes_made.append(f'Completed: {line.strip()} → {lines[i].strip()}')
|
|
228
|
+
break
|
|
229
|
+
modified_content = '\n'.join(lines)
|
|
230
|
+
if not completed:
|
|
231
|
+
changes_made.append(f'Item not found for completion: {item}')
|
|
232
|
+
|
|
233
|
+
elif action == 'uncompleted':
|
|
234
|
+
# Mark item as uncomplete: - [x] → - [ ]
|
|
235
|
+
if item:
|
|
236
|
+
lines = modified_content.split('\n')
|
|
237
|
+
uncompleted = False
|
|
238
|
+
for i, line in enumerate(lines):
|
|
239
|
+
if item in line and '- [x]' in line:
|
|
240
|
+
lines[i] = line.replace('- [x]', '- [ ]')
|
|
241
|
+
uncompleted = True
|
|
242
|
+
changes_made.append(f'Uncompleted: {line.strip()} → {lines[i].strip()}')
|
|
243
|
+
break
|
|
244
|
+
modified_content = '\n'.join(lines)
|
|
245
|
+
if not uncompleted:
|
|
246
|
+
changes_made.append(f'Item not found for uncompletion: {item}')
|
|
247
|
+
|
|
248
|
+
# If we made any add/remove/complete/uncomplete changes, write the updated content
|
|
249
|
+
if any(change.startswith(('Added:', 'Removed:', 'Completed:', 'Uncompleted:')) for change in
|
|
250
|
+
changes_made):
|
|
251
|
+
await file_system.write_file('todo.md', modified_content + '\n')
|
|
252
|
+
|
|
253
|
+
changes_summary = '\n'.join(changes_made) if changes_made else 'No changes made'
|
|
254
|
+
|
|
255
|
+
logger.info(f'✏️ Modified todo.md: {len(changes_made)} changes')
|
|
256
|
+
return ActionResult(
|
|
257
|
+
extracted_content=f'Todo modifications completed:\n{changes_summary}\n\nUpdated content:\n{modified_content}',
|
|
258
|
+
long_term_memory=f'Modified todo list: {len(changes_made)} changes made',
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f'❌ Failed to modify todo file: {e}')
|
|
263
|
+
raise RuntimeError(f'Failed to modify todo file: {str(e)}')
|
|
264
|
+
|
|
265
|
+
def _register_done_action(self):
|
|
266
|
+
@self.registry.action(
|
|
267
|
+
'Complete task and output final response. Use for simple responses or comprehensive markdown summaries with optional follow-up task suggestions.',
|
|
268
|
+
param_model=VibeSurfDoneAction,
|
|
269
|
+
)
|
|
270
|
+
async def task_done(
|
|
271
|
+
params: VibeSurfDoneAction,
|
|
272
|
+
):
|
|
273
|
+
"""
|
|
274
|
+
Complete task execution and provide final response.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
def _register_file_actions(self):
|
|
280
|
+
@self.registry.action(
|
|
281
|
+
'Replace old_str with new_str in file_name. old_str must exactly match the string to replace in original text. Recommended tool to mark completed items in todo.md or change specific contents in a file.'
|
|
282
|
+
)
|
|
283
|
+
async def replace_file_str(file_name: str, old_str: str, new_str: str, file_system: CustomFileSystem):
|
|
284
|
+
result = await file_system.replace_file_str(file_name, old_str, new_str)
|
|
285
|
+
logger.info(f'💾 {result}')
|
|
286
|
+
return ActionResult(extracted_content=result, long_term_memory=result)
|
|
287
|
+
|
|
288
|
+
@self.registry.action(
|
|
289
|
+
'Read file content from file system. If this is a file not in current file system, please provide an absolute path.')
|
|
290
|
+
async def read_file(file_path: str, file_system: CustomFileSystem):
|
|
291
|
+
if os.path.exists(file_path):
|
|
292
|
+
external_file = True
|
|
293
|
+
else:
|
|
294
|
+
external_file = False
|
|
295
|
+
result = await file_system.read_file(file_path, external_file=external_file)
|
|
296
|
+
|
|
297
|
+
MAX_MEMORY_SIZE = 1000
|
|
298
|
+
if len(result) > MAX_MEMORY_SIZE:
|
|
299
|
+
lines = result.splitlines()
|
|
300
|
+
display = ''
|
|
301
|
+
lines_count = 0
|
|
302
|
+
for line in lines:
|
|
303
|
+
if len(display) + len(line) < MAX_MEMORY_SIZE:
|
|
304
|
+
display += line + '\n'
|
|
305
|
+
lines_count += 1
|
|
306
|
+
else:
|
|
307
|
+
break
|
|
308
|
+
remaining_lines = len(lines) - lines_count
|
|
309
|
+
memory = f'{display}{remaining_lines} more lines...' if remaining_lines > 0 else display
|
|
310
|
+
else:
|
|
311
|
+
memory = result
|
|
312
|
+
logger.info(f'💾 {memory}')
|
|
313
|
+
return ActionResult(
|
|
314
|
+
extracted_content=result,
|
|
315
|
+
include_in_memory=True,
|
|
316
|
+
long_term_memory=memory,
|
|
317
|
+
include_extracted_content_only_once=True,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@self.registry.action(
|
|
321
|
+
'Extract content from a file. Support image files, pdf, markdown, txt, json, csv.',
|
|
322
|
+
param_model=FileExtractionAction,
|
|
323
|
+
)
|
|
324
|
+
async def extract_content_from_file(
|
|
325
|
+
params: FileExtractionAction,
|
|
326
|
+
page_extraction_llm: BaseChatModel,
|
|
327
|
+
file_system: CustomFileSystem,
|
|
328
|
+
):
|
|
329
|
+
try:
|
|
330
|
+
# Get file path
|
|
331
|
+
file_path = params.file_path
|
|
332
|
+
full_file_path = file_path
|
|
333
|
+
# Check if file exists
|
|
334
|
+
if not os.path.exists(full_file_path):
|
|
335
|
+
full_file_path = os.path.join(str(file_system.get_dir()), file_path)
|
|
336
|
+
|
|
337
|
+
# Determine if file is an image based on MIME type
|
|
338
|
+
mime_type, _ = mimetypes.guess_type(file_path)
|
|
339
|
+
is_image = mime_type and mime_type.startswith('image/')
|
|
340
|
+
|
|
341
|
+
if is_image:
|
|
342
|
+
# Handle image files with LLM vision
|
|
343
|
+
try:
|
|
344
|
+
# Read image file and encode to base64
|
|
345
|
+
with open(full_file_path, 'rb') as image_file:
|
|
346
|
+
image_data = image_file.read()
|
|
347
|
+
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
|
348
|
+
|
|
349
|
+
# Create content parts similar to the user's example
|
|
350
|
+
content_parts: list[ContentPartTextParam | ContentPartImageParam] = [
|
|
351
|
+
ContentPartTextParam(text=f"Query: {params.query}")
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
# Add the image
|
|
355
|
+
content_parts.append(
|
|
356
|
+
ContentPartImageParam(
|
|
357
|
+
image_url=ImageURL(
|
|
358
|
+
url=f'data:{mime_type};base64,{image_base64}',
|
|
359
|
+
media_type=mime_type,
|
|
360
|
+
detail='high',
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Create user message and invoke LLM
|
|
366
|
+
user_message = UserMessage(content=content_parts, cache=True)
|
|
367
|
+
response = await asyncio.wait_for(
|
|
368
|
+
page_extraction_llm.ainvoke([user_message]),
|
|
369
|
+
timeout=120.0,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
extracted_content = f'File: {file_path}\nQuery: {params.query}\nExtracted Content:\n{response.completion}'
|
|
373
|
+
|
|
374
|
+
except Exception as e:
|
|
375
|
+
raise Exception(f'Failed to process image file {file_path}: {str(e)}')
|
|
376
|
+
|
|
377
|
+
else:
|
|
378
|
+
# Handle non-image files by reading content
|
|
379
|
+
try:
|
|
380
|
+
file_content = await file_system.read_file(full_file_path, external_file=True)
|
|
381
|
+
|
|
382
|
+
# Create a simple prompt for text extraction
|
|
383
|
+
prompt = f"""Extract the requested information from this file content.
|
|
384
|
+
|
|
385
|
+
Query: {params.query}
|
|
386
|
+
|
|
387
|
+
File: {file_path}
|
|
388
|
+
File Content:
|
|
389
|
+
{file_content}
|
|
390
|
+
|
|
391
|
+
Provide the extracted information in a clear, structured format."""
|
|
392
|
+
|
|
393
|
+
response = await asyncio.wait_for(
|
|
394
|
+
page_extraction_llm.ainvoke([UserMessage(content=prompt)]),
|
|
395
|
+
timeout=120.0,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
extracted_content = f'File: {file_path}\nQuery: {params.query}\nExtracted Content:\n{response.completion}'
|
|
399
|
+
|
|
400
|
+
except Exception as e:
|
|
401
|
+
raise Exception(f'Failed to read file {file_path}: {str(e)}')
|
|
402
|
+
|
|
403
|
+
# Handle memory storage
|
|
404
|
+
if len(extracted_content) < 1000:
|
|
405
|
+
memory = extracted_content
|
|
406
|
+
include_extracted_content_only_once = False
|
|
407
|
+
else:
|
|
408
|
+
save_result = await file_system.save_extracted_content(extracted_content)
|
|
409
|
+
memory = (
|
|
410
|
+
f'Extracted content from file {file_path} for query: {params.query}\nContent saved to file system: {save_result}'
|
|
411
|
+
)
|
|
412
|
+
include_extracted_content_only_once = True
|
|
413
|
+
|
|
414
|
+
logger.info(f'📄 Extracted content from file: {file_path}')
|
|
415
|
+
return ActionResult(
|
|
416
|
+
extracted_content=extracted_content,
|
|
417
|
+
include_extracted_content_only_once=include_extracted_content_only_once,
|
|
418
|
+
long_term_memory=memory,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.debug(f'Error extracting content from file: {e}')
|
|
423
|
+
raise RuntimeError(str(e))
|
|
424
|
+
|
|
425
|
+
@self.registry.action(
|
|
426
|
+
'Write or append content to file_path in file system. Allowed extensions are .md, .txt, .json, .csv, .pdf. For .pdf files, write the content in markdown format and it will automatically be converted to a properly formatted PDF document.'
|
|
427
|
+
)
|
|
428
|
+
async def write_file(
|
|
429
|
+
file_path: str,
|
|
430
|
+
content: str,
|
|
431
|
+
file_system: FileSystem,
|
|
432
|
+
append: bool = False,
|
|
433
|
+
trailing_newline: bool = True,
|
|
434
|
+
leading_newline: bool = False,
|
|
435
|
+
):
|
|
436
|
+
if trailing_newline:
|
|
437
|
+
content += '\n'
|
|
438
|
+
if leading_newline:
|
|
439
|
+
content = '\n' + content
|
|
440
|
+
if append:
|
|
441
|
+
result = await file_system.append_file(file_path, content)
|
|
442
|
+
else:
|
|
443
|
+
result = await file_system.write_file(file_path, content)
|
|
444
|
+
logger.info(f'💾 {result}')
|
|
445
|
+
return ActionResult(extracted_content=result, long_term_memory=result)
|
|
446
|
+
|
|
447
|
+
@self.registry.action(
|
|
448
|
+
'Copy a file to the FileSystem. Set external_src=True to copy from external file(absolute path)to FileSystem, False to copy within FileSystem.'
|
|
449
|
+
)
|
|
450
|
+
async def copy_file(src_file_path: str, dst_file_path: str, file_system: CustomFileSystem,
|
|
451
|
+
external_src: bool = False):
|
|
452
|
+
result = await file_system.copy_file(src_file_path, dst_file_path, external_src)
|
|
453
|
+
logger.info(f'📁 {result}')
|
|
454
|
+
return ActionResult(
|
|
455
|
+
extracted_content=result,
|
|
456
|
+
include_in_memory=True,
|
|
457
|
+
long_term_memory=result,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
@self.registry.action(
|
|
461
|
+
'Rename a file to new_filename. src_file_path is a relative path to the FileSystem.'
|
|
462
|
+
)
|
|
463
|
+
async def rename_file(src_file_path: str, new_filename: str, file_system: CustomFileSystem):
|
|
464
|
+
result = await file_system.rename_file(src_file_path, new_filename)
|
|
465
|
+
logger.info(f'📁 {result}')
|
|
466
|
+
return ActionResult(
|
|
467
|
+
extracted_content=result,
|
|
468
|
+
include_in_memory=True,
|
|
469
|
+
long_term_memory=result,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
@self.registry.action(
|
|
473
|
+
'Move a file within the FileSystem from old_filename to new_filename.'
|
|
474
|
+
)
|
|
475
|
+
async def move_file(old_file_path: str, new_file_path: str, file_system: CustomFileSystem):
|
|
476
|
+
result = await file_system.move_file(old_file_path, new_file_path)
|
|
477
|
+
logger.info(f'📁 {result}')
|
|
478
|
+
return ActionResult(
|
|
479
|
+
extracted_content=result,
|
|
480
|
+
include_in_memory=True,
|
|
481
|
+
long_term_memory=result,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
@self.registry.action(
|
|
485
|
+
'Check file exist or not.'
|
|
486
|
+
)
|
|
487
|
+
async def file_exist(file_path: str, file_system: CustomFileSystem):
|
|
488
|
+
if os.path.exists(file_path):
|
|
489
|
+
result = f"{file_path} is a external file and it exists."
|
|
490
|
+
else:
|
|
491
|
+
is_file_exist = await file_system.file_exist(file_path)
|
|
492
|
+
if is_file_exist:
|
|
493
|
+
result = f"{file_path} is in file system and it exists."
|
|
494
|
+
else:
|
|
495
|
+
result = f"{file_path} does not exists."
|
|
496
|
+
|
|
497
|
+
logger.info(f'📁 {result}')
|
|
498
|
+
return ActionResult(
|
|
499
|
+
extracted_content=result,
|
|
500
|
+
include_in_memory=True,
|
|
501
|
+
long_term_memory=result,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
@self.registry.action(
|
|
505
|
+
'List contents of a directory within the FileSystem. Use empty string "" or "." to list the root data_dir, or provide relative path for subdirectory.'
|
|
506
|
+
)
|
|
507
|
+
async def list_directory(directory_path: str, file_system: CustomFileSystem):
|
|
508
|
+
result = await file_system.list_directory(directory_path)
|
|
509
|
+
logger.info(f'📁 {result}')
|
|
510
|
+
return ActionResult(
|
|
511
|
+
extracted_content=result,
|
|
512
|
+
include_in_memory=True,
|
|
513
|
+
long_term_memory=result,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
@self.registry.action(
|
|
517
|
+
'Create a directory within the FileSystem.'
|
|
518
|
+
)
|
|
519
|
+
async def create_directory(directory_path: str, file_system: CustomFileSystem):
|
|
520
|
+
result = await file_system.create_directory(directory_path)
|
|
521
|
+
logger.info(f'📁 {result}')
|
|
522
|
+
return ActionResult(
|
|
523
|
+
extracted_content=result,
|
|
524
|
+
include_in_memory=True,
|
|
525
|
+
long_term_memory=result,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
async def register_mcp_clients(self, mcp_server_config: Optional[Dict[str, Any]] = None):
|
|
529
|
+
self.mcp_server_config = mcp_server_config or self.mcp_server_config
|
|
530
|
+
if self.mcp_server_config:
|
|
531
|
+
await self.unregister_mcp_clients()
|
|
532
|
+
await self.register_mcp_tools()
|
|
533
|
+
|
|
534
|
+
async def register_mcp_tools(self):
|
|
535
|
+
"""
|
|
536
|
+
Register the MCP tools used by this tools.
|
|
537
|
+
"""
|
|
538
|
+
if not self.mcp_server_config:
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
# Handle both formats: with or without "mcpServers" key
|
|
542
|
+
mcp_servers = self.mcp_server_config.get('mcpServers', self.mcp_server_config)
|
|
543
|
+
|
|
544
|
+
if not mcp_servers:
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
for server_name, server_config in mcp_servers.items():
|
|
548
|
+
try:
|
|
549
|
+
logger.info(f'Connecting to MCP server: {server_name}')
|
|
550
|
+
|
|
551
|
+
# Create MCP client
|
|
552
|
+
client = CustomMCPClient(
|
|
553
|
+
server_name=server_name,
|
|
554
|
+
command=server_config['command'],
|
|
555
|
+
args=server_config['args'],
|
|
556
|
+
env=server_config.get('env', None)
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Connect to the MCP server
|
|
560
|
+
await client.connect(timeout=200)
|
|
561
|
+
|
|
562
|
+
# Register tools to tools with prefix
|
|
563
|
+
prefix = f"mcp.{server_name}."
|
|
564
|
+
await client.register_to_tools(
|
|
565
|
+
tools=self,
|
|
566
|
+
prefix=prefix
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Store client for later cleanup
|
|
570
|
+
self.mcp_clients[server_name] = client
|
|
571
|
+
|
|
572
|
+
logger.info(f'Successfully registered MCP server: {server_name} with prefix: {prefix}')
|
|
573
|
+
|
|
574
|
+
except Exception as e:
|
|
575
|
+
logger.error(f'Failed to register MCP server {server_name}: {str(e)}')
|
|
576
|
+
# Continue with other servers even if one fails
|
|
577
|
+
|
|
578
|
+
async def unregister_mcp_clients(self):
|
|
579
|
+
"""
|
|
580
|
+
Unregister and disconnect all MCP clients.
|
|
581
|
+
"""
|
|
582
|
+
# Disconnect all MCP clients
|
|
583
|
+
for server_name, client in self.mcp_clients.items():
|
|
584
|
+
try:
|
|
585
|
+
logger.info(f'Disconnecting MCP server: {server_name}')
|
|
586
|
+
await client.disconnect()
|
|
587
|
+
except Exception as e:
|
|
588
|
+
logger.error(f'Failed to disconnect MCP server {server_name}: {str(e)}')
|
|
589
|
+
|
|
590
|
+
# Remove MCP tools from registry
|
|
591
|
+
try:
|
|
592
|
+
# Get all registered actions
|
|
593
|
+
actions_to_remove = []
|
|
594
|
+
for action_name in list(self.registry.registry.actions.keys()):
|
|
595
|
+
if action_name.startswith('mcp.'):
|
|
596
|
+
actions_to_remove.append(action_name)
|
|
597
|
+
|
|
598
|
+
# Remove MCP actions from registry
|
|
599
|
+
for action_name in actions_to_remove:
|
|
600
|
+
if action_name in self.registry.registry.actions:
|
|
601
|
+
del self.registry.registry.actions[action_name]
|
|
602
|
+
logger.info(f'Removed MCP action: {action_name}')
|
|
603
|
+
|
|
604
|
+
except Exception as e:
|
|
605
|
+
logger.error(f'Failed to remove MCP actions from registry: {str(e)}')
|
|
606
|
+
|
|
607
|
+
# Clear the clients dictionary
|
|
608
|
+
self.mcp_clients.clear()
|
|
609
|
+
logger.info('All MCP clients unregistered and disconnected')
|
|
610
|
+
|
|
611
|
+
@time_execution_sync('--act')
|
|
612
|
+
async def act(
|
|
613
|
+
self,
|
|
614
|
+
action: ActionModel,
|
|
615
|
+
browser_manager: BrowserManager | None = None,
|
|
616
|
+
llm: BaseChatModel | None = None,
|
|
617
|
+
file_system: CustomFileSystem | None = None,
|
|
618
|
+
) -> ActionResult:
|
|
619
|
+
"""Execute an action"""
|
|
620
|
+
|
|
621
|
+
for action_name, params in action.model_dump(exclude_unset=True).items():
|
|
622
|
+
if params is not None:
|
|
623
|
+
try:
|
|
624
|
+
if action_name not in self.registry.registry.actions:
|
|
625
|
+
raise ValueError(f'Action {action_name} not found')
|
|
626
|
+
action = self.registry.registry.actions[action_name]
|
|
627
|
+
special_context = {
|
|
628
|
+
'browser_manager': browser_manager,
|
|
629
|
+
'page_extraction_llm': llm,
|
|
630
|
+
'file_system': file_system,
|
|
631
|
+
}
|
|
632
|
+
try:
|
|
633
|
+
validated_params = action.param_model(**params)
|
|
634
|
+
except Exception as e:
|
|
635
|
+
raise ValueError(f'Invalid parameters {params} for action {action_name}: {type(e)}: {e}') from e
|
|
636
|
+
|
|
637
|
+
result = await action.function(params=validated_params, **special_context)
|
|
638
|
+
except BrowserError as e:
|
|
639
|
+
logger.error(f'❌ Action {action_name} failed with BrowserError: {str(e)}')
|
|
640
|
+
result = handle_browser_error(e)
|
|
641
|
+
except TimeoutError as e:
|
|
642
|
+
logger.error(f'❌ Action {action_name} failed with TimeoutError: {str(e)}')
|
|
643
|
+
result = ActionResult(error=f'{action_name} was not executed due to timeout.')
|
|
644
|
+
except Exception as e:
|
|
645
|
+
# Log the original exception with traceback for observability
|
|
646
|
+
logger.error(f"Action '{action_name}' failed with error: {str(e)}")
|
|
647
|
+
result = ActionResult(error=str(e))
|
|
648
|
+
|
|
649
|
+
if isinstance(result, str):
|
|
650
|
+
return ActionResult(extracted_content=result)
|
|
651
|
+
elif isinstance(result, ActionResult):
|
|
652
|
+
return result
|
|
653
|
+
elif result is None:
|
|
654
|
+
return ActionResult()
|
|
655
|
+
else:
|
|
656
|
+
raise ValueError(f'Invalid action result type: {type(result)} of {result}')
|
|
657
|
+
return ActionResult()
|