autobyteus 1.2.0__py3-none-any.whl → 1.2.1__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.
- autobyteus/agent/context/agent_runtime_state.py +4 -0
- autobyteus/agent/events/notifiers.py +5 -1
- autobyteus/agent/message/send_message_to.py +5 -4
- autobyteus/agent/streaming/agent_event_stream.py +5 -0
- autobyteus/agent/streaming/stream_event_payloads.py +25 -0
- autobyteus/agent/streaming/stream_events.py +13 -1
- autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +4 -4
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +12 -12
- autobyteus/agent_team/context/agent_team_runtime_state.py +2 -2
- autobyteus/agent_team/streaming/agent_team_event_notifier.py +4 -4
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +3 -3
- autobyteus/agent_team/streaming/agent_team_stream_events.py +8 -8
- autobyteus/agent_team/task_notification/activation_policy.py +1 -1
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +22 -22
- autobyteus/agent_team/task_notification/task_notification_mode.py +1 -1
- autobyteus/cli/agent_team_tui/app.py +4 -4
- autobyteus/cli/agent_team_tui/state.py +8 -8
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +3 -3
- autobyteus/cli/agent_team_tui/widgets/shared.py +1 -1
- autobyteus/cli/agent_team_tui/widgets/{task_board_panel.py → task_plan_panel.py} +5 -5
- autobyteus/events/event_types.py +4 -3
- autobyteus/multimedia/audio/api/__init__.py +3 -2
- autobyteus/multimedia/audio/api/openai_audio_client.py +112 -0
- autobyteus/multimedia/audio/audio_client_factory.py +37 -0
- autobyteus/multimedia/image/image_client_factory.py +1 -1
- autobyteus/task_management/__init__.py +43 -20
- autobyteus/task_management/{base_task_board.py → base_task_plan.py} +16 -13
- autobyteus/task_management/converters/__init__.py +2 -2
- autobyteus/task_management/converters/{task_board_converter.py → task_plan_converter.py} +13 -13
- autobyteus/task_management/events.py +7 -7
- autobyteus/task_management/{in_memory_task_board.py → in_memory_task_plan.py} +34 -22
- autobyteus/task_management/schemas/__init__.py +3 -0
- autobyteus/task_management/schemas/task_status_report.py +2 -2
- autobyteus/task_management/schemas/todo_definition.py +15 -0
- autobyteus/task_management/todo.py +29 -0
- autobyteus/task_management/todo_list.py +75 -0
- autobyteus/task_management/tools/__init__.py +24 -8
- autobyteus/task_management/tools/task_tools/__init__.py +19 -0
- autobyteus/task_management/tools/{assign_task_to.py → task_tools/assign_task_to.py} +18 -18
- autobyteus/task_management/tools/{publish_task.py → task_tools/create_task.py} +16 -18
- autobyteus/task_management/tools/{publish_tasks.py → task_tools/create_tasks.py} +19 -19
- autobyteus/task_management/tools/{get_my_tasks.py → task_tools/get_my_tasks.py} +15 -15
- autobyteus/task_management/tools/{get_task_board_status.py → task_tools/get_task_plan_status.py} +16 -16
- autobyteus/task_management/tools/{update_task_status.py → task_tools/update_task_status.py} +16 -16
- autobyteus/task_management/tools/todo_tools/__init__.py +18 -0
- autobyteus/task_management/tools/todo_tools/add_todo.py +78 -0
- autobyteus/task_management/tools/todo_tools/create_todo_list.py +79 -0
- autobyteus/task_management/tools/todo_tools/get_todo_list.py +55 -0
- autobyteus/task_management/tools/todo_tools/update_todo_status.py +85 -0
- autobyteus/tools/__init__.py +15 -11
- autobyteus/tools/bash/bash_executor.py +3 -3
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +5 -5
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +4 -4
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -3
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -3
- autobyteus/tools/browser/standalone/navigate_to.py +13 -9
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +9 -5
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +10 -6
- autobyteus/tools/browser/standalone/webpage_reader.py +13 -9
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +9 -5
- autobyteus/tools/file/__init__.py +13 -0
- autobyteus/tools/file/{file_editor.py → edit_file.py} +11 -11
- autobyteus/tools/file/list_directory.py +168 -0
- autobyteus/tools/file/{file_reader.py → read_file.py} +3 -3
- autobyteus/tools/file/search_files.py +188 -0
- autobyteus/tools/file/{file_writer.py → write_file.py} +3 -3
- autobyteus/tools/functional_tool.py +10 -8
- autobyteus/tools/mcp/tool.py +3 -3
- autobyteus/tools/mcp/tool_registrar.py +5 -2
- autobyteus/tools/multimedia/__init__.py +2 -1
- autobyteus/tools/multimedia/audio_tools.py +2 -2
- autobyteus/tools/{download_media_tool.py → multimedia/download_media_tool.py} +3 -3
- autobyteus/tools/multimedia/image_tools.py +4 -4
- autobyteus/tools/multimedia/media_reader_tool.py +1 -1
- autobyteus/tools/registry/tool_definition.py +66 -13
- autobyteus/tools/registry/tool_registry.py +29 -0
- autobyteus/tools/search/__init__.py +17 -0
- autobyteus/tools/search/base_strategy.py +35 -0
- autobyteus/tools/search/client.py +24 -0
- autobyteus/tools/search/factory.py +81 -0
- autobyteus/tools/search/google_cse_strategy.py +68 -0
- autobyteus/tools/search/providers.py +10 -0
- autobyteus/tools/search/serpapi_strategy.py +65 -0
- autobyteus/tools/search/serper_strategy.py +87 -0
- autobyteus/tools/search_tool.py +83 -0
- autobyteus/tools/timer.py +4 -0
- autobyteus/tools/tool_meta.py +4 -24
- autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +1 -2
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/METADATA +5 -5
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/RECORD +95 -80
- examples/run_agentic_software_engineer.py +239 -0
- examples/run_poem_writer.py +3 -3
- autobyteus/person/__init__.py +0 -0
- autobyteus/person/examples/__init__.py +0 -0
- autobyteus/person/examples/sample_persons.py +0 -14
- autobyteus/person/examples/sample_roles.py +0 -14
- autobyteus/person/person.py +0 -29
- autobyteus/person/role.py +0 -14
- autobyteus/tools/google_search.py +0 -149
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/WHEEL +0 -0
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -23,7 +23,11 @@ class NavigateTo(BaseTool, UIIntegrator):
|
|
|
23
23
|
def __init__(self, config: Optional[ToolConfig] = None):
|
|
24
24
|
BaseTool.__init__(self, config=config)
|
|
25
25
|
UIIntegrator.__init__(self)
|
|
26
|
-
logger.debug("
|
|
26
|
+
logger.debug("navigate_to (standalone) tool initialized.")
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def get_name(cls) -> str:
|
|
30
|
+
return "navigate_to"
|
|
27
31
|
|
|
28
32
|
@classmethod
|
|
29
33
|
def get_description(cls) -> str:
|
|
@@ -41,33 +45,33 @@ class NavigateTo(BaseTool, UIIntegrator):
|
|
|
41
45
|
return schema
|
|
42
46
|
|
|
43
47
|
async def _execute(self, context: 'AgentContext', url: str) -> str:
|
|
44
|
-
logger.info(f"
|
|
48
|
+
logger.info(f"navigate_to (standalone) for agent {context.agent_id} navigating to: {url}")
|
|
45
49
|
|
|
46
50
|
if not self._is_valid_url(url):
|
|
47
51
|
error_msg = f"Invalid URL format: {url}. Must include scheme (e.g., http, https) and netloc."
|
|
48
|
-
logger.warning(f"
|
|
52
|
+
logger.warning(f"navigate_to (standalone) validation error for agent {context.agent_id}: {error_msg}")
|
|
49
53
|
raise ValueError(error_msg)
|
|
50
54
|
|
|
51
55
|
try:
|
|
52
56
|
await self.initialize()
|
|
53
57
|
if not self.page:
|
|
54
|
-
logger.error("Playwright page not initialized in
|
|
55
|
-
raise RuntimeError("Playwright page not available for
|
|
58
|
+
logger.error("Playwright page not initialized in navigate_to (standalone).")
|
|
59
|
+
raise RuntimeError("Playwright page not available for navigate_to.")
|
|
56
60
|
|
|
57
61
|
response = await self.page.goto(url, wait_until="domcontentloaded", timeout=60000)
|
|
58
62
|
|
|
59
63
|
if response and response.ok:
|
|
60
64
|
success_msg = f"Successfully navigated to {url}"
|
|
61
|
-
logger.info(f"
|
|
65
|
+
logger.info(f"navigate_to (standalone) for agent {context.agent_id}: {success_msg}")
|
|
62
66
|
return success_msg
|
|
63
67
|
else:
|
|
64
68
|
status = response.status if response else "Unknown"
|
|
65
69
|
failure_msg = f"Navigation to {url} failed with status {status}"
|
|
66
|
-
logger.warning(f"
|
|
70
|
+
logger.warning(f"navigate_to (standalone) for agent {context.agent_id}: {failure_msg}")
|
|
67
71
|
return failure_msg
|
|
68
72
|
except Exception as e:
|
|
69
|
-
logger.error(f"Error during
|
|
70
|
-
raise RuntimeError(f"
|
|
73
|
+
logger.error(f"Error during navigate_to (standalone) for URL '{url}', agent {context.agent_id}: {e}", exc_info=True)
|
|
74
|
+
raise RuntimeError(f"navigate_to (standalone) failed for URL '{url}': {str(e)}")
|
|
71
75
|
finally:
|
|
72
76
|
await self.close()
|
|
73
77
|
|
|
@@ -24,7 +24,11 @@ class WebPagePDFGenerator(BaseTool, UIIntegrator):
|
|
|
24
24
|
def __init__(self, config: Optional[ToolConfig] = None):
|
|
25
25
|
BaseTool.__init__(self, config=config)
|
|
26
26
|
UIIntegrator.__init__(self)
|
|
27
|
-
logger.debug("
|
|
27
|
+
logger.debug("generate_webpage_pdf (standalone) tool initialized.")
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_name(cls) -> str:
|
|
31
|
+
return "generate_webpage_pdf"
|
|
28
32
|
|
|
29
33
|
@classmethod
|
|
30
34
|
def get_description(cls) -> str:
|
|
@@ -49,7 +53,7 @@ class WebPagePDFGenerator(BaseTool, UIIntegrator):
|
|
|
49
53
|
return schema
|
|
50
54
|
|
|
51
55
|
async def _execute(self, context: 'AgentContext', url: str, save_dir: str) -> str:
|
|
52
|
-
logger.info(f"
|
|
56
|
+
logger.info(f"generate_webpage_pdf for agent {context.agent_id} generating PDF for '{url}', saving to directory '{save_dir}'.")
|
|
53
57
|
|
|
54
58
|
if not self._is_valid_page_url(url):
|
|
55
59
|
raise ValueError(f"Invalid page URL format: {url}. Must be a full URL (e.g., http/https).")
|
|
@@ -73,8 +77,8 @@ class WebPagePDFGenerator(BaseTool, UIIntegrator):
|
|
|
73
77
|
try:
|
|
74
78
|
await self.initialize()
|
|
75
79
|
if not self.page:
|
|
76
|
-
logger.error("Playwright page not initialized in
|
|
77
|
-
raise RuntimeError("Playwright page not available for
|
|
80
|
+
logger.error("Playwright page not initialized in generate_webpage_pdf.")
|
|
81
|
+
raise RuntimeError("Playwright page not available for generate_webpage_pdf.")
|
|
78
82
|
|
|
79
83
|
await self.page.goto(url, wait_until="networkidle", timeout=60000)
|
|
80
84
|
|
|
@@ -85,7 +89,7 @@ class WebPagePDFGenerator(BaseTool, UIIntegrator):
|
|
|
85
89
|
return absolute_file_path
|
|
86
90
|
except Exception as e:
|
|
87
91
|
logger.error(f"Error generating PDF for URL '{url}': {e}", exc_info=True)
|
|
88
|
-
raise RuntimeError(f"
|
|
92
|
+
raise RuntimeError(f"generate_webpage_pdf failed for URL '{url}': {str(e)}")
|
|
89
93
|
finally:
|
|
90
94
|
await self.close()
|
|
91
95
|
|
|
@@ -24,7 +24,11 @@ class WebPageImageDownloader(BaseTool, UIIntegrator):
|
|
|
24
24
|
def __init__(self, config: Optional[ToolConfig] = None):
|
|
25
25
|
BaseTool.__init__(self, config=config)
|
|
26
26
|
UIIntegrator.__init__(self)
|
|
27
|
-
logger.debug("
|
|
27
|
+
logger.debug("download_webpage_images tool initialized.")
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_name(cls) -> str:
|
|
31
|
+
return "download_webpage_images"
|
|
28
32
|
|
|
29
33
|
@classmethod
|
|
30
34
|
def get_description(cls) -> str:
|
|
@@ -49,7 +53,7 @@ class WebPageImageDownloader(BaseTool, UIIntegrator):
|
|
|
49
53
|
return schema
|
|
50
54
|
|
|
51
55
|
async def _execute(self, context: 'AgentContext', url: str, save_dir: str) -> List[str]:
|
|
52
|
-
logger.info(f"
|
|
56
|
+
logger.info(f"download_webpage_images for agent {context.agent_id} downloading images from '{url}' to '{save_dir}'.")
|
|
53
57
|
|
|
54
58
|
if not self._is_valid_page_url(url):
|
|
55
59
|
raise ValueError(f"Invalid page URL format: {url}. Must be a full URL (e.g., http/https).")
|
|
@@ -60,8 +64,8 @@ class WebPageImageDownloader(BaseTool, UIIntegrator):
|
|
|
60
64
|
try:
|
|
61
65
|
await self.initialize()
|
|
62
66
|
if not self.page:
|
|
63
|
-
logger.error("Playwright page not initialized in
|
|
64
|
-
raise RuntimeError("Playwright page not available for
|
|
67
|
+
logger.error("Playwright page not initialized in download_webpage_images.")
|
|
68
|
+
raise RuntimeError("Playwright page not available for download_webpage_images.")
|
|
65
69
|
|
|
66
70
|
await self.page.goto(url, wait_until="networkidle", timeout=60000)
|
|
67
71
|
|
|
@@ -104,8 +108,8 @@ class WebPageImageDownloader(BaseTool, UIIntegrator):
|
|
|
104
108
|
return saved_paths
|
|
105
109
|
|
|
106
110
|
except Exception as e:
|
|
107
|
-
logger.error(f"Error in
|
|
108
|
-
raise RuntimeError(f"
|
|
111
|
+
logger.error(f"Error in download_webpage_images for URL '{url}': {e}", exc_info=True)
|
|
112
|
+
raise RuntimeError(f"download_webpage_images failed for URL '{url}': {str(e)}")
|
|
109
113
|
finally:
|
|
110
114
|
await self.close()
|
|
111
115
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
File: autobyteus/tools/browser/standalone/webpage_reader.py
|
|
3
|
-
This module provides a
|
|
3
|
+
This module provides a read_webpage tool for reading and cleaning HTML content from webpages.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
@@ -35,15 +35,19 @@ class WebPageReader(BaseTool, UIIntegrator):
|
|
|
35
35
|
try:
|
|
36
36
|
cleaning_mode_to_use = CleaningMode(cleaning_mode_value.upper())
|
|
37
37
|
except ValueError:
|
|
38
|
-
logger.warning(f"Invalid cleaning_mode string '{cleaning_mode_value}' in config for
|
|
38
|
+
logger.warning(f"Invalid cleaning_mode string '{cleaning_mode_value}' in config for read_webpage. Using THOROUGH.")
|
|
39
39
|
cleaning_mode_to_use = CleaningMode.THOROUGH
|
|
40
40
|
elif isinstance(cleaning_mode_value, CleaningMode):
|
|
41
41
|
cleaning_mode_to_use = cleaning_mode_value
|
|
42
42
|
else:
|
|
43
|
-
logger.warning(f"Invalid type for cleaning_mode in config for
|
|
43
|
+
logger.warning(f"Invalid type for cleaning_mode in config for read_webpage. Using THOROUGH.")
|
|
44
44
|
|
|
45
45
|
self.cleaning_mode = cleaning_mode_to_use
|
|
46
|
-
logger.debug(f"
|
|
46
|
+
logger.debug(f"read_webpage initialized with cleaning_mode: {self.cleaning_mode}")
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_name(cls) -> str:
|
|
50
|
+
return "read_webpage"
|
|
47
51
|
|
|
48
52
|
@classmethod
|
|
49
53
|
def get_description(cls) -> str:
|
|
@@ -63,7 +67,7 @@ class WebPageReader(BaseTool, UIIntegrator):
|
|
|
63
67
|
|
|
64
68
|
@classmethod
|
|
65
69
|
def get_config_schema(cls) -> Optional[ParameterSchema]:
|
|
66
|
-
"""Schema for parameters to configure the
|
|
70
|
+
"""Schema for parameters to configure the read_webpage instance itself."""
|
|
67
71
|
schema = ParameterSchema()
|
|
68
72
|
schema.add_parameter(ParameterDefinition(
|
|
69
73
|
name="cleaning_mode",
|
|
@@ -76,13 +80,13 @@ class WebPageReader(BaseTool, UIIntegrator):
|
|
|
76
80
|
return schema
|
|
77
81
|
|
|
78
82
|
async def _execute(self, context: 'AgentContext', url: str) -> str:
|
|
79
|
-
logger.info(f"
|
|
83
|
+
logger.info(f"read_webpage executing for agent {context.agent_id} with URL: '{url}'")
|
|
80
84
|
|
|
81
85
|
try:
|
|
82
86
|
await self.initialize()
|
|
83
87
|
if not self.page:
|
|
84
|
-
logger.error("Playwright page not initialized in
|
|
85
|
-
raise RuntimeError("Playwright page not available for
|
|
88
|
+
logger.error("Playwright page not initialized in read_webpage.")
|
|
89
|
+
raise RuntimeError("Playwright page not available for read_webpage.")
|
|
86
90
|
|
|
87
91
|
await self.page.goto(url, timeout=60000, wait_until="domcontentloaded")
|
|
88
92
|
page_content = await self.page.content()
|
|
@@ -96,6 +100,6 @@ class WebPageReader(BaseTool, UIIntegrator):
|
|
|
96
100
|
'''
|
|
97
101
|
except Exception as e:
|
|
98
102
|
logger.error(f"Error reading webpage at URL '{url}': {e}", exc_info=True)
|
|
99
|
-
raise RuntimeError(f"
|
|
103
|
+
raise RuntimeError(f"read_webpage failed for URL '{url}': {str(e)}")
|
|
100
104
|
finally:
|
|
101
105
|
await self.close()
|
|
@@ -31,7 +31,11 @@ class WebPageScreenshotTaker(BaseTool, UIIntegrator):
|
|
|
31
31
|
if self.image_format not in ["png", "jpeg"]:
|
|
32
32
|
logger.warning(f"Invalid image_format '{self.image_format}' in config. Defaulting to 'png'.")
|
|
33
33
|
self.image_format = "png"
|
|
34
|
-
logger.debug(f"
|
|
34
|
+
logger.debug(f"take_webpage_screenshot initialized. Full page: {self.full_page}, Format: {self.image_format}")
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def get_name(cls) -> str:
|
|
38
|
+
return "take_webpage_screenshot"
|
|
35
39
|
|
|
36
40
|
@classmethod
|
|
37
41
|
def get_description(cls) -> str:
|
|
@@ -75,7 +79,7 @@ class WebPageScreenshotTaker(BaseTool, UIIntegrator):
|
|
|
75
79
|
return schema
|
|
76
80
|
|
|
77
81
|
async def _execute(self, context: 'AgentContext', url: str, file_path: str) -> str:
|
|
78
|
-
logger.info(f"
|
|
82
|
+
logger.info(f"take_webpage_screenshot for agent {context.agent_id} taking screenshot of '{url}', saving to '{file_path}'.")
|
|
79
83
|
|
|
80
84
|
output_dir = os.path.dirname(file_path)
|
|
81
85
|
if output_dir:
|
|
@@ -84,8 +88,8 @@ class WebPageScreenshotTaker(BaseTool, UIIntegrator):
|
|
|
84
88
|
try:
|
|
85
89
|
await self.initialize()
|
|
86
90
|
if not self.page:
|
|
87
|
-
logger.error("Playwright page not initialized in
|
|
88
|
-
raise RuntimeError("Playwright page not available for
|
|
91
|
+
logger.error("Playwright page not initialized in take_webpage_screenshot.")
|
|
92
|
+
raise RuntimeError("Playwright page not available for take_webpage_screenshot.")
|
|
89
93
|
|
|
90
94
|
await self.page.goto(url, wait_until="networkidle", timeout=60000)
|
|
91
95
|
|
|
@@ -96,6 +100,6 @@ class WebPageScreenshotTaker(BaseTool, UIIntegrator):
|
|
|
96
100
|
return absolute_file_path
|
|
97
101
|
except Exception as e:
|
|
98
102
|
logger.error(f"Error taking screenshot of URL '{url}': {e}", exc_info=True)
|
|
99
|
-
raise RuntimeError(f"
|
|
103
|
+
raise RuntimeError(f"take_webpage_screenshot failed for URL '{url}': {str(e)}")
|
|
100
104
|
finally:
|
|
101
105
|
await self.close()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .edit_file import edit_file
|
|
2
|
+
from .read_file import read_file
|
|
3
|
+
from .write_file import write_file
|
|
4
|
+
from .search_files import search_files
|
|
5
|
+
from .list_directory import list_directory
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"edit_file",
|
|
9
|
+
"read_file",
|
|
10
|
+
"write_file",
|
|
11
|
+
"search_files",
|
|
12
|
+
"list_directory",
|
|
13
|
+
]
|
|
@@ -21,7 +21,7 @@ def _resolve_file_path(context: 'AgentContext', path: str) -> str:
|
|
|
21
21
|
"""Resolves an absolute path for the given input, using the agent workspace when needed."""
|
|
22
22
|
if os.path.isabs(path):
|
|
23
23
|
final_path = path
|
|
24
|
-
logger.debug("
|
|
24
|
+
logger.debug("edit_file: provided path '%s' is absolute.", path)
|
|
25
25
|
else:
|
|
26
26
|
if not context.workspace:
|
|
27
27
|
error_msg = ("Relative path '%s' provided, but no workspace is configured for agent '%s'. "
|
|
@@ -35,10 +35,10 @@ def _resolve_file_path(context: 'AgentContext', path: str) -> str:
|
|
|
35
35
|
logger.error(error_msg, context.agent_id, base_path, path)
|
|
36
36
|
raise ValueError(error_msg % (context.agent_id, base_path, path))
|
|
37
37
|
final_path = os.path.join(base_path, path)
|
|
38
|
-
logger.debug("
|
|
38
|
+
logger.debug("edit_file: resolved relative path '%s' against workspace base '%s' to '%s'.", path, base_path, final_path)
|
|
39
39
|
|
|
40
40
|
normalized_path = os.path.normpath(final_path)
|
|
41
|
-
logger.debug("
|
|
41
|
+
logger.debug("edit_file: normalized path to '%s'.", normalized_path)
|
|
42
42
|
return normalized_path
|
|
43
43
|
|
|
44
44
|
|
|
@@ -56,7 +56,7 @@ def _apply_unified_diff(original_lines: List[str], patch: str) -> List[str]:
|
|
|
56
56
|
line = patch_lines[line_idx]
|
|
57
57
|
|
|
58
58
|
if line.startswith('---') or line.startswith('+++'):
|
|
59
|
-
logger.debug("
|
|
59
|
+
logger.debug("edit_file: skipping diff header line '%s'.", line.strip())
|
|
60
60
|
line_idx += 1
|
|
61
61
|
continue
|
|
62
62
|
|
|
@@ -75,7 +75,7 @@ def _apply_unified_diff(original_lines: List[str], patch: str) -> List[str]:
|
|
|
75
75
|
old_count = int(match.group('old_count') or '1')
|
|
76
76
|
new_start = int(match.group('new_start'))
|
|
77
77
|
new_count = int(match.group('new_count') or '1')
|
|
78
|
-
logger.debug("
|
|
78
|
+
logger.debug("edit_file: processing hunk old_start=%s old_count=%s new_start=%s new_count=%s.",
|
|
79
79
|
old_start, old_count, new_start, new_count)
|
|
80
80
|
|
|
81
81
|
target_idx = old_start - 1 if old_start > 0 else 0
|
|
@@ -152,8 +152,8 @@ def _apply_unified_diff(original_lines: List[str], patch: str) -> List[str]:
|
|
|
152
152
|
return patched_lines
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
@tool(name="
|
|
156
|
-
async def
|
|
155
|
+
@tool(name="edit_file", category=ToolCategory.FILE_SYSTEM)
|
|
156
|
+
async def edit_file(context: 'AgentContext', path: str, patch: str, create_if_missing: bool = False) -> str:
|
|
157
157
|
"""Applies a unified diff patch to update a text file without overwriting unrelated content.
|
|
158
158
|
|
|
159
159
|
Args:
|
|
@@ -166,7 +166,7 @@ async def file_edit(context: 'AgentContext', path: str, patch: str, create_if_mi
|
|
|
166
166
|
PatchApplicationError: If the patch content cannot be applied cleanly.
|
|
167
167
|
IOError: If file reading or writing fails.
|
|
168
168
|
"""
|
|
169
|
-
logger.debug("
|
|
169
|
+
logger.debug("edit_file: requested edit for agent '%s' on path '%s'.", context.agent_id, path)
|
|
170
170
|
final_path = _resolve_file_path(context, path)
|
|
171
171
|
|
|
172
172
|
dir_path = os.path.dirname(final_path)
|
|
@@ -190,11 +190,11 @@ async def file_edit(context: 'AgentContext', path: str, patch: str, create_if_mi
|
|
|
190
190
|
with open(final_path, 'w', encoding='utf-8') as destination:
|
|
191
191
|
destination.writelines(patched_lines)
|
|
192
192
|
|
|
193
|
-
logger.info("
|
|
193
|
+
logger.info("edit_file: successfully applied patch to '%s'.", final_path)
|
|
194
194
|
return f"File edited successfully at {final_path}"
|
|
195
195
|
except PatchApplicationError as patch_err:
|
|
196
|
-
logger.error("
|
|
196
|
+
logger.error("edit_file: failed to apply patch to '%s': %s", final_path, patch_err, exc_info=True)
|
|
197
197
|
raise patch_err
|
|
198
198
|
except Exception as exc: # pragma: no cover - general safeguard
|
|
199
|
-
logger.error("
|
|
199
|
+
logger.error("edit_file: unexpected error while editing '%s': %s", final_path, exc, exc_info=True)
|
|
200
200
|
raise IOError(f"Could not edit file at '{final_path}': {exc}")
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/file/list_directory.py
|
|
2
|
+
"""
|
|
3
|
+
This module provides a tool for listing directory contents in a structured,
|
|
4
|
+
tree-like format, mirroring the behavior of the Codex Rust implementation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from collections import deque
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import List, Deque, Tuple, Optional, TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from autobyteus.tools.functional_tool import tool
|
|
16
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from autobyteus.agent.context import AgentContext
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Constants from the design document
|
|
24
|
+
INDENTATION_SPACES = 2
|
|
25
|
+
MAX_ENTRY_LENGTH = 500
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class DirEntry:
|
|
29
|
+
"""Represents a collected directory entry for sorting and formatting."""
|
|
30
|
+
name: str
|
|
31
|
+
kind: str
|
|
32
|
+
depth: int
|
|
33
|
+
|
|
34
|
+
@tool(name="list_directory", category=ToolCategory.FILE_SYSTEM)
|
|
35
|
+
async def list_directory(
|
|
36
|
+
context: 'AgentContext',
|
|
37
|
+
path: str,
|
|
38
|
+
depth: int = 2,
|
|
39
|
+
limit: int = 25,
|
|
40
|
+
offset: int = 1
|
|
41
|
+
) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Lists the contents of a directory in a structured, tree-like format.
|
|
44
|
+
|
|
45
|
+
This tool performs a breadth-first traversal of the specified directory up to a
|
|
46
|
+
given depth. It returns a deterministic, lexicographically sorted list of entries,
|
|
47
|
+
formatted with indentation and tree glyphs to represent the hierarchy.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path: The path to the directory to list. Relative paths are resolved against the agent's workspace.
|
|
51
|
+
depth: The maximum directory depth to traverse. Must be > 0.
|
|
52
|
+
limit: The maximum number of entries to return in the output. Must be > 0.
|
|
53
|
+
offset: The 1-indexed entry number to start from, for pagination. Must be > 0.
|
|
54
|
+
"""
|
|
55
|
+
# --- 1. Argument Validation ---
|
|
56
|
+
logger.debug(f"list_directory for agent {context.agent_id}, initial path: {path}")
|
|
57
|
+
|
|
58
|
+
final_path: str
|
|
59
|
+
if os.path.isabs(path):
|
|
60
|
+
final_path = path
|
|
61
|
+
logger.debug(f"Path '{path}' is absolute. Using it directly.")
|
|
62
|
+
else:
|
|
63
|
+
if not context.workspace:
|
|
64
|
+
error_msg = f"Relative path '{path}' provided, but no workspace is configured for agent '{context.agent_id}'. A workspace is required to resolve relative paths."
|
|
65
|
+
logger.error(error_msg)
|
|
66
|
+
raise ValueError(error_msg)
|
|
67
|
+
|
|
68
|
+
base_path = context.workspace.get_base_path()
|
|
69
|
+
if not base_path or not isinstance(base_path, str):
|
|
70
|
+
error_msg = f"Agent '{context.agent_id}' has a configured workspace, but it provided an invalid base path ('{base_path}'). Cannot resolve relative path '{path}'."
|
|
71
|
+
logger.error(error_msg)
|
|
72
|
+
raise ValueError(error_msg)
|
|
73
|
+
|
|
74
|
+
final_path = os.path.join(base_path, path)
|
|
75
|
+
logger.debug(f"Path '{path}' is relative. Resolved to '{final_path}' using workspace base path '{base_path}'.")
|
|
76
|
+
|
|
77
|
+
final_path = os.path.normpath(final_path)
|
|
78
|
+
|
|
79
|
+
if not Path(final_path).is_dir():
|
|
80
|
+
raise FileNotFoundError(f"Directory not found at path: {final_path}")
|
|
81
|
+
if depth <= 0 or limit <= 0 or offset <= 0:
|
|
82
|
+
raise ValueError("depth, limit, and offset must all be greater than zero.")
|
|
83
|
+
|
|
84
|
+
# --- 2. Asynchronous Traversal ---
|
|
85
|
+
loop = asyncio.get_running_loop()
|
|
86
|
+
all_entries = await loop.run_in_executor(
|
|
87
|
+
None, _traverse_directory_bfs, Path(final_path), depth
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# --- 3. Slicing ---
|
|
91
|
+
total_found = len(all_entries)
|
|
92
|
+
start_index = offset - 1
|
|
93
|
+
end_index = start_index + limit
|
|
94
|
+
sliced_entries = all_entries[start_index:end_index]
|
|
95
|
+
|
|
96
|
+
# --- 4. Formatting ---
|
|
97
|
+
output_lines = [f"Absolute path: {final_path}"]
|
|
98
|
+
|
|
99
|
+
# To correctly apply tree glyphs, we need to know which entry is the last in its directory
|
|
100
|
+
# This is complex with BFS. A simpler, visually acceptable approach is taken here.
|
|
101
|
+
# For a more accurate glyph representation like the Rust version, we would need to
|
|
102
|
+
# process entries directory by directory after collection.
|
|
103
|
+
for i, entry in enumerate(sliced_entries):
|
|
104
|
+
# A simplified glyph logic: last item in the slice gets the closing glyph
|
|
105
|
+
is_last = (i == len(sliced_entries) - 1)
|
|
106
|
+
output_lines.append(_format_entry_line(entry, is_last))
|
|
107
|
+
|
|
108
|
+
if total_found > end_index:
|
|
109
|
+
output_lines.append(f"More than {limit} entries found.")
|
|
110
|
+
|
|
111
|
+
return "\n".join(output_lines)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _traverse_directory_bfs(start_path: Path, max_depth: int) -> List[DirEntry]:
|
|
115
|
+
"""
|
|
116
|
+
Performs a breadth-first traversal of a directory. This is a synchronous function
|
|
117
|
+
designed to be run in a thread pool executor.
|
|
118
|
+
"""
|
|
119
|
+
collected: List[DirEntry] = []
|
|
120
|
+
queue: Deque[Tuple[Path, int]] = deque([(start_path, 0)])
|
|
121
|
+
|
|
122
|
+
while queue:
|
|
123
|
+
current_path, current_depth = queue.popleft()
|
|
124
|
+
|
|
125
|
+
if current_depth >= max_depth:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
# Use os.scandir for efficiency as it fetches file type info
|
|
130
|
+
entries_at_level = []
|
|
131
|
+
for entry in os.scandir(current_path):
|
|
132
|
+
kind = "[unknown]"
|
|
133
|
+
if entry.is_dir():
|
|
134
|
+
kind = "[dir]"
|
|
135
|
+
queue.append((Path(entry.path), current_depth + 1))
|
|
136
|
+
elif entry.is_file():
|
|
137
|
+
kind = "[file]"
|
|
138
|
+
elif entry.is_symlink():
|
|
139
|
+
kind = "[link]"
|
|
140
|
+
|
|
141
|
+
# Truncate long filenames
|
|
142
|
+
display_name = entry.name
|
|
143
|
+
if len(display_name) > MAX_ENTRY_LENGTH:
|
|
144
|
+
display_name = display_name[:MAX_ENTRY_LENGTH] + "..."
|
|
145
|
+
|
|
146
|
+
entries_at_level.append(DirEntry(name=display_name, kind=kind, depth=current_depth + 1))
|
|
147
|
+
|
|
148
|
+
# Sort entries at the current level before adding to the main list
|
|
149
|
+
entries_at_level.sort(key=lambda e: e.name)
|
|
150
|
+
collected.extend(entries_at_level)
|
|
151
|
+
|
|
152
|
+
except (PermissionError, OSError) as e:
|
|
153
|
+
logger.warning(f"Could not read directory '{current_path}': {e}")
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
return collected
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _format_entry_line(entry: DirEntry, is_last_in_slice: bool) -> str:
|
|
160
|
+
"""Formats a single directory entry into its final string representation."""
|
|
161
|
+
# This simplified glyph logic doesn't know about siblings, just the slice.
|
|
162
|
+
# A full implementation would require grouping by parent path after collection.
|
|
163
|
+
prefix = "└─ " if is_last_in_slice else "├─ "
|
|
164
|
+
|
|
165
|
+
# Indentation is based on depth from the root search path
|
|
166
|
+
indentation = " " * INDENTATION_SPACES * (entry.depth -1)
|
|
167
|
+
|
|
168
|
+
return f"{indentation}{prefix}{entry.kind} {entry.name}"
|
|
@@ -10,8 +10,8 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
|
-
@tool(name="
|
|
14
|
-
async def
|
|
13
|
+
@tool(name="read_file", category=ToolCategory.FILE_SYSTEM)
|
|
14
|
+
async def read_file(context: 'AgentContext', path: str) -> str:
|
|
15
15
|
"""
|
|
16
16
|
Reads content from a specified file.
|
|
17
17
|
'path' is the path to the file. If relative, it must be resolved against a configured agent workspace.
|
|
@@ -19,7 +19,7 @@ async def file_reader(context: 'AgentContext', path: str) -> str:
|
|
|
19
19
|
Raises FileNotFoundError if the file does not exist.
|
|
20
20
|
Raises IOError if file reading fails for other reasons.
|
|
21
21
|
"""
|
|
22
|
-
logger.debug(f"Functional
|
|
22
|
+
logger.debug(f"Functional read_file tool for agent {context.agent_id}, initial path: {path}")
|
|
23
23
|
|
|
24
24
|
final_path: str
|
|
25
25
|
if os.path.isabs(path):
|