microbots 0.0.3__tar.gz → 0.0.4__tar.gz
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.
- {microbots-0.0.3 → microbots-0.0.4}/.gitignore +0 -1
- {microbots-0.0.3 → microbots-0.0.4}/PKG-INFO +1 -1
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/MicroBot.py +3 -33
- microbots-0.0.4/src/microbots/bot/LogAnalysisBot.py +66 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/ReadingBot.py +2 -7
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/WritingBot.py +2 -7
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/constants.py +1 -0
- microbots-0.0.4/src/microbots/environment/Environment.py +35 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/LocalDockerEnvironment.py +132 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/image_builder/ShellCommunicator.py +35 -47
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/image_builder/dockerShell.py +14 -0
- microbots-0.0.4/src/microbots/utils/path.py +39 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/PKG-INFO +1 -1
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/SOURCES.txt +5 -0
- microbots-0.0.4/test/bot/calculator/calculator.log +15 -0
- microbots-0.0.4/test/bot/calculator/code/calculator.py +50 -0
- microbots-0.0.4/test/bot/calculator/log_analysis_test.py +30 -0
- microbots-0.0.4/test/environment/local_docker/TestFileCopy.py +59 -0
- microbots-0.0.3/src/microbots/environment/Environment.py +0 -23
- microbots-0.0.3/src/microbots/utils/path.py +0 -21
- {microbots-0.0.3 → microbots-0.0.4}/.github/workflows/dockerBuildPush.yml +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/.github/workflows/publish.yml +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/LICENSE +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/README.md +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/architechture.md +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/docs/images/overall_architecture.png +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/pyproject.toml +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/pytest.ini +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/requirements.txt +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/setup.cfg +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/__init__.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/BrowsingBot.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/CustomBot.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/__init__.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/__init__.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/image_builder/Dockerfile +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/swe-rex/LocalDocker.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/llm/__init__.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/llm/openai_api.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_definitions/browser-use/browser.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_definitions/browser-use.yaml +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_definitions/cscope.yaml +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_test.ipynb +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/utils/logger.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots/utils/network.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/dependency_links.txt +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/requires.txt +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/top_level.txt +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/test/bot/browsing_bot_test.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/test/bot/countries_to_capital/countries_dir/countries.txt +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/test/bot/countries_to_capital/reading_bot_test.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/test/bot/countries_to_capital/writing_bot_test.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/test/environment/local_docker/LocalDockerEnvironmentTest.py +0 -0
- {microbots-0.0.3 → microbots-0.0.4}/test/environment/swe-rex/LocalDockerTest.py +0 -0
|
@@ -4,7 +4,6 @@ import time
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from enum import StrEnum
|
|
6
6
|
from logging import getLogger
|
|
7
|
-
from pathlib import Path
|
|
8
7
|
from typing import Optional
|
|
9
8
|
|
|
10
9
|
from microbots.constants import ModelProvider, PermissionLabels, PermissionMapping
|
|
@@ -15,12 +14,7 @@ from microbots.llm.openai_api import OpenAIApi
|
|
|
15
14
|
from microbots.tools.tool import Tool, install_tools, setup_tools
|
|
16
15
|
from microbots.utils.logger import LogLevelEmoji, LogTextColor
|
|
17
16
|
from microbots.utils.network import get_free_port
|
|
18
|
-
from microbots.utils.path import
|
|
19
|
-
get_absolute_path,
|
|
20
|
-
get_base_name,
|
|
21
|
-
is_absolute_path,
|
|
22
|
-
is_valid_path,
|
|
23
|
-
)
|
|
17
|
+
from microbots.utils.path import get_path_info
|
|
24
18
|
|
|
25
19
|
logger = getLogger(" MicroBot ")
|
|
26
20
|
|
|
@@ -47,6 +41,7 @@ class BotType(StrEnum):
|
|
|
47
41
|
WRITING_BOT = "WRITING_BOT"
|
|
48
42
|
BROWSING_BOT = "BROWSING_BOT"
|
|
49
43
|
CUSTOM_BOT = "CUSTOM_BOT"
|
|
44
|
+
LOG_ANALYSIS_BOT = "LOG_ANALYSIS_BOT"
|
|
50
45
|
|
|
51
46
|
|
|
52
47
|
@dataclass
|
|
@@ -56,31 +51,6 @@ class BotRunResult:
|
|
|
56
51
|
error: Optional[str]
|
|
57
52
|
|
|
58
53
|
|
|
59
|
-
@dataclass
|
|
60
|
-
class FolderMountInfo:
|
|
61
|
-
path_valid: bool
|
|
62
|
-
base_name: str
|
|
63
|
-
abs_path: str
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def get_folder_mount_info(folder_to_mount: str) -> FolderMountInfo:
|
|
67
|
-
return_value = FolderMountInfo(
|
|
68
|
-
path_valid=False,
|
|
69
|
-
base_name="",
|
|
70
|
-
abs_path="",
|
|
71
|
-
)
|
|
72
|
-
if is_absolute_path(folder_to_mount):
|
|
73
|
-
return_value.path_valid = is_valid_path(folder_to_mount)
|
|
74
|
-
return_value.abs_path = folder_to_mount
|
|
75
|
-
return_value.base_name = get_base_name(folder_to_mount)
|
|
76
|
-
else:
|
|
77
|
-
return_value.abs_path = get_absolute_path(folder_to_mount)
|
|
78
|
-
return_value.path_valid = is_valid_path(return_value.abs_path)
|
|
79
|
-
return_value.base_name = folder_to_mount
|
|
80
|
-
|
|
81
|
-
return return_value
|
|
82
|
-
|
|
83
|
-
|
|
84
54
|
class MicroBot:
|
|
85
55
|
|
|
86
56
|
def __init__(
|
|
@@ -96,7 +66,7 @@ class MicroBot:
|
|
|
96
66
|
# validate init values before assigning
|
|
97
67
|
self.permission = permission
|
|
98
68
|
if folder_to_mount is not None:
|
|
99
|
-
folder_mount_info =
|
|
69
|
+
folder_mount_info = get_path_info(folder_to_mount)
|
|
100
70
|
if folder_mount_info.path_valid is False:
|
|
101
71
|
raise ValueError(
|
|
102
72
|
f"Invalid folder to mount: {folder_to_mount} resolved to {folder_mount_info.abs_path}"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from microbots.constants import DOCKER_WORKING_DIR, LOG_FILE_DIR, PermissionLabels
|
|
5
|
+
from microbots.MicroBot import BotType, MicroBot, system_prompt_common
|
|
6
|
+
from microbots.tools.tool import Tool
|
|
7
|
+
from microbots.utils.path import get_path_info
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LogAnalysisBot(MicroBot):
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
model: str,
|
|
17
|
+
folder_to_mount: str,
|
|
18
|
+
environment: Optional[any] = None,
|
|
19
|
+
additional_tools: Optional[list[Tool]] = [],
|
|
20
|
+
):
|
|
21
|
+
# validate init values before assigning
|
|
22
|
+
bot_type = BotType.LOG_ANALYSIS_BOT
|
|
23
|
+
permission = PermissionLabels.READ_ONLY
|
|
24
|
+
|
|
25
|
+
folder_mount_info = get_path_info(folder_to_mount)
|
|
26
|
+
base_name = folder_mount_info.base_name
|
|
27
|
+
|
|
28
|
+
system_prompt = f"""
|
|
29
|
+
{system_prompt_common}
|
|
30
|
+
You are a helpful log analysis bot. Your job is to analyze a log file and identify the root-cause if there are any failure. You'll be given read-only access to the code from where the log is generated. The read-only code is available at /{DOCKER_WORKING_DIR}/{base_name}.
|
|
31
|
+
|
|
32
|
+
The log file to analyze will be given in the user prompt. You can find the provided log file under the directory /{LOG_FILE_DIR}/
|
|
33
|
+
|
|
34
|
+
Only when you have run all necessary commands and identified the root cause, you should give me the final result.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
super().__init__(
|
|
38
|
+
bot_type,
|
|
39
|
+
model,
|
|
40
|
+
system_prompt,
|
|
41
|
+
environment,
|
|
42
|
+
additional_tools,
|
|
43
|
+
folder_to_mount,
|
|
44
|
+
permission,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def run(self, file_name: str, timeout_in_seconds: int = 300) -> any:
|
|
48
|
+
|
|
49
|
+
# Add the logic to copy the file from the user path to /var/log path in container
|
|
50
|
+
file_mount_info = get_path_info(file_name)
|
|
51
|
+
if not file_mount_info.path_valid:
|
|
52
|
+
raise ValueError(f"file name {file_name} is not a valid path")
|
|
53
|
+
|
|
54
|
+
# Copy the file to the container
|
|
55
|
+
copy_to_container_result = self.environment.copy_to_container(
|
|
56
|
+
file_mount_info.abs_path, f"/var/log/{file_mount_info.base_name}"
|
|
57
|
+
)
|
|
58
|
+
if copy_to_container_result is False:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Failed to copy file to container: {file_mount_info.base_name}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
file_name_prompt = f"""
|
|
64
|
+
The log file to analyze is {file_name} which is present in {LOG_FILE_DIR} directory.
|
|
65
|
+
"""
|
|
66
|
+
return super().run(file_name_prompt, timeout_in_seconds)
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from microbots.constants import DOCKER_WORKING_DIR, PermissionLabels
|
|
4
|
-
from microbots.MicroBot import
|
|
5
|
-
BotType,
|
|
6
|
-
MicroBot,
|
|
7
|
-
get_folder_mount_info,
|
|
8
|
-
system_prompt_common,
|
|
9
|
-
)
|
|
4
|
+
from microbots.MicroBot import BotType, MicroBot, get_path_info, system_prompt_common
|
|
10
5
|
from microbots.tools.tool import Tool
|
|
11
6
|
|
|
12
7
|
|
|
@@ -23,7 +18,7 @@ class ReadingBot(MicroBot):
|
|
|
23
18
|
bot_type = BotType.READING_BOT
|
|
24
19
|
permission = PermissionLabels.READ_ONLY
|
|
25
20
|
|
|
26
|
-
folder_mount_info =
|
|
21
|
+
folder_mount_info = get_path_info(folder_to_mount)
|
|
27
22
|
base_name = folder_mount_info.base_name
|
|
28
23
|
|
|
29
24
|
system_prompt = f"""
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from microbots.constants import DOCKER_WORKING_DIR, PermissionLabels
|
|
4
|
-
from microbots.MicroBot import
|
|
5
|
-
BotType,
|
|
6
|
-
MicroBot,
|
|
7
|
-
get_folder_mount_info,
|
|
8
|
-
system_prompt_common,
|
|
9
|
-
)
|
|
4
|
+
from microbots.MicroBot import BotType, MicroBot, get_path_info, system_prompt_common
|
|
10
5
|
from microbots.tools.tool import Tool
|
|
11
6
|
|
|
12
7
|
|
|
@@ -23,7 +18,7 @@ class WritingBot(MicroBot):
|
|
|
23
18
|
bot_type = BotType.WRITING_BOT
|
|
24
19
|
permission = PermissionLabels.READ_WRITE
|
|
25
20
|
|
|
26
|
-
folder_mount_info =
|
|
21
|
+
folder_mount_info = get_path_info(folder_to_mount)
|
|
27
22
|
base_name = folder_mount_info.base_name
|
|
28
23
|
|
|
29
24
|
system_prompt = f"""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class CmdReturn:
|
|
7
|
+
stdout: str
|
|
8
|
+
stderr: str
|
|
9
|
+
return_code: int
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Environment(ABC):
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def start(self):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def stop(self):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def execute(self, command: str, timeout: Optional[int] = 300) -> CmdReturn:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def copy_to_container(self, src_path: str, dest_path: str) -> bool:
|
|
26
|
+
raise NotImplementedError(
|
|
27
|
+
f"{self.__class__.__name__} does not support copying files to container. "
|
|
28
|
+
f"This is an optional feature - only implement if needed for your use case."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def copy_from_container(self, src_path: str, dest_path: str) -> bool:
|
|
32
|
+
raise NotImplementedError(
|
|
33
|
+
f"{self.__class__.__name__} does not support copying files from container. "
|
|
34
|
+
f"This is an optional feature - only implement if needed for your use case."
|
|
35
|
+
)
|
{microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/LocalDockerEnvironment.py
RENAMED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
3
5
|
import time
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Optional
|
|
@@ -158,3 +160,133 @@ class LocalDockerEnvironment(Environment):
|
|
|
158
160
|
except Exception as e:
|
|
159
161
|
logger.exception("❌ Unexpected error while executing command: %s", e)
|
|
160
162
|
return CmdReturn(stdout="", stderr="Unexpected error", return_code=1)
|
|
163
|
+
def copy_to_container(self, src_path: str, dest_path: str) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Copy a file or folder from the host machine to the Docker container.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
src_path: Path to the source file/folder on the host machine
|
|
169
|
+
dest_path: Destination path inside the container
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
bool: True if copy was successful, False otherwise
|
|
173
|
+
"""
|
|
174
|
+
if not self.container:
|
|
175
|
+
logger.error("❌ No active container to copy to")
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Check if source path exists
|
|
180
|
+
if not os.path.exists(src_path):
|
|
181
|
+
logger.error("❌ Source path does not exist: %s", src_path)
|
|
182
|
+
return False
|
|
183
|
+
# Ensure destination directory exists inside container
|
|
184
|
+
dest_dir = os.path.dirname(dest_path)
|
|
185
|
+
if dest_dir and dest_dir != '/':
|
|
186
|
+
# Check if directory exists inside the container first
|
|
187
|
+
check_cmd = f"test -d {shlex.quote(dest_dir)}"
|
|
188
|
+
check_result = self.execute(check_cmd)
|
|
189
|
+
|
|
190
|
+
if check_result.return_code != 0:
|
|
191
|
+
logger.debug("📁 Creating destination directory inside container: %s", dest_dir)
|
|
192
|
+
mkdir_cmd = f"mkdir -p {shlex.quote(dest_dir)}"
|
|
193
|
+
mkdir_result = self.execute(mkdir_cmd)
|
|
194
|
+
|
|
195
|
+
if mkdir_result.return_code != 0:
|
|
196
|
+
logger.error("❌ Failed to create destination directory %s: %s",
|
|
197
|
+
dest_dir, mkdir_result.stderr)
|
|
198
|
+
return False
|
|
199
|
+
else:
|
|
200
|
+
logger.debug("✅ Destination directory created: %s", dest_dir)
|
|
201
|
+
else:
|
|
202
|
+
logger.debug("✅ Destination directory already exists: %s", dest_dir)
|
|
203
|
+
|
|
204
|
+
# Use docker cp command to copy files/folders
|
|
205
|
+
# Escape paths for shell safety
|
|
206
|
+
|
|
207
|
+
# Build docker cp command
|
|
208
|
+
cmd = ["docker", "cp", src_path, f"{self.container.id}:{dest_path}"]
|
|
209
|
+
|
|
210
|
+
logger.debug("📁 Copying %s to container:%s", src_path, dest_path)
|
|
211
|
+
|
|
212
|
+
# Execute the copy command
|
|
213
|
+
result = subprocess.run(
|
|
214
|
+
cmd,
|
|
215
|
+
shell=True,
|
|
216
|
+
capture_output=True,
|
|
217
|
+
text=True,
|
|
218
|
+
timeout=300
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if result.returncode == 0:
|
|
222
|
+
logger.info("✅ Successfully copied %s to container:%s", src_path, dest_path)
|
|
223
|
+
return True
|
|
224
|
+
else:
|
|
225
|
+
logger.error("❌ Failed to copy file. Error: %s", result.stderr)
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
except subprocess.TimeoutExpired:
|
|
229
|
+
logger.error("❌ Copy operation timed out after 300 seconds")
|
|
230
|
+
return False
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.exception("❌ Unexpected error during copy operation: %s", e)
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def copy_from_container(self, src_path: str, dest_path: str) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
Copy a file or folder from the Docker container to the host machine.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
src_path: Path to the source file/folder inside the container
|
|
241
|
+
dest_path: Destination path on the host machine
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
bool: True if copy was successful, False otherwise
|
|
245
|
+
"""
|
|
246
|
+
if not self.container:
|
|
247
|
+
logger.error("❌ No active container to copy from")
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
# Check if source path exists inside the container
|
|
252
|
+
check_cmd = f"test -e {shlex.quote(src_path)}"
|
|
253
|
+
check_result = self.execute(check_cmd)
|
|
254
|
+
|
|
255
|
+
if check_result.return_code != 0:
|
|
256
|
+
logger.error("❌ Source path does not exist in container: %s", src_path)
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
# Check if destination directory exists on host machine
|
|
260
|
+
dest_dir = os.path.dirname(dest_path)
|
|
261
|
+
if not os.path.exists(dest_dir):
|
|
262
|
+
logger.error("❌ Destination directory does not exist on host: %s", dest_dir)
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
cmd = ["docker", "cp", f"{self.container.id}:{src_path}", dest_path]
|
|
266
|
+
|
|
267
|
+
# Build docker cp command
|
|
268
|
+
|
|
269
|
+
logger.debug("📁 Copying container:%s to %s", src_path, dest_path)
|
|
270
|
+
|
|
271
|
+
# Execute the copy command
|
|
272
|
+
result = subprocess.run(
|
|
273
|
+
cmd,
|
|
274
|
+
shell=True,
|
|
275
|
+
capture_output=True,
|
|
276
|
+
text=True,
|
|
277
|
+
timeout=300
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if result.returncode == 0:
|
|
281
|
+
logger.info("✅ Successfully copied from container:%s to %s", src_path, dest_path)
|
|
282
|
+
return True
|
|
283
|
+
else:
|
|
284
|
+
logger.error("❌ Failed to copy file. Error: %s", result.stderr)
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
except subprocess.TimeoutExpired:
|
|
288
|
+
logger.error("❌ Copy operation timed out after 300 seconds")
|
|
289
|
+
return False
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.exception("❌ Unexpected error during copy operation: %s", e)
|
|
292
|
+
return False
|
|
@@ -34,10 +34,12 @@ class ShellCommunicator:
|
|
|
34
34
|
Initialize the shell communicator.
|
|
35
35
|
|
|
36
36
|
Args:
|
|
37
|
-
shell_type: Type of shell (
|
|
37
|
+
shell_type: Type of shell (only "bash" is supported)
|
|
38
38
|
encoding: Text encoding for communication
|
|
39
39
|
"""
|
|
40
40
|
self.shell_type = shell_type.lower()
|
|
41
|
+
if self.shell_type != "bash":
|
|
42
|
+
raise ValueError(f"Unsupported shell type: {shell_type}. Only 'bash' is supported.")
|
|
41
43
|
self.encoding = encoding
|
|
42
44
|
self.process: Optional[subprocess.Popen] = None
|
|
43
45
|
self.output_queue = queue.Queue()
|
|
@@ -47,13 +49,9 @@ class ShellCommunicator:
|
|
|
47
49
|
self.error_thread: Optional[threading.Thread] = None
|
|
48
50
|
self.output_callback: Optional[Callable] = None
|
|
49
51
|
|
|
50
|
-
# Define shell commands
|
|
52
|
+
# Define shell commands - only supporting bash
|
|
51
53
|
self.shell_commands = {
|
|
52
|
-
"powershell": ["powershell.exe", "-NoLogo", "-NoExit"],
|
|
53
|
-
"cmd": ["cmd.exe", "/k"],
|
|
54
54
|
"bash": ["bash"],
|
|
55
|
-
"python": [sys.executable, "-i"],
|
|
56
|
-
"wsl": ["wsl.exe"],
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
def start_session(self) -> bool:
|
|
@@ -134,7 +132,6 @@ class ShellCommunicator:
|
|
|
134
132
|
# command = command.replace("<", "<").replace(">", ">")
|
|
135
133
|
return command
|
|
136
134
|
|
|
137
|
-
# TODO: Exit code is not properly captured. Need to fix it.
|
|
138
135
|
def send_command(
|
|
139
136
|
self, command: str, wait_for_output: bool = True, timeout: float = 300
|
|
140
137
|
) -> CmdReturn:
|
|
@@ -155,28 +152,24 @@ class ShellCommunicator:
|
|
|
155
152
|
|
|
156
153
|
try:
|
|
157
154
|
command = self._re_escape(command)
|
|
158
|
-
|
|
159
|
-
self.process.stdin.write(command + "\n")
|
|
160
|
-
self.process.stdin.flush()
|
|
161
|
-
logger.debug("➡️ Sent command: %s", command)
|
|
162
|
-
|
|
155
|
+
|
|
163
156
|
if not wait_for_output:
|
|
157
|
+
# Send the command without marker for async execution
|
|
158
|
+
self.process.stdin.write(command + "\n")
|
|
159
|
+
self.process.stdin.flush()
|
|
160
|
+
logger.debug("➡️ Sent async command: %s", command)
|
|
164
161
|
return CmdReturn(stdout="ASYNC: Not waiting for completion", stderr="", return_code=0)
|
|
165
162
|
|
|
166
163
|
# Generate a unique command completion marker
|
|
167
164
|
marker = f"__COMMAND_COMPLETE_{int(time.time() * 1000000)}__"
|
|
168
165
|
|
|
169
|
-
# Send
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
elif self.shell_type == "cmd":
|
|
175
|
-
self.process.stdin.write(f"echo {marker} & echo %ERRORLEVEL%\n")
|
|
176
|
-
elif self.shell_type == "python":
|
|
177
|
-
self.process.stdin.write(f"print('{marker}')\n")
|
|
178
|
-
|
|
166
|
+
# For bash only: Send command + marker in a single line to capture correct exit code
|
|
167
|
+
combined_command = f"{command}; echo '{marker}' $?"
|
|
168
|
+
|
|
169
|
+
# Send the combined command
|
|
170
|
+
self.process.stdin.write(combined_command + "\n")
|
|
179
171
|
self.process.stdin.flush()
|
|
172
|
+
logger.debug("➡️ Sent command: %s", command)
|
|
180
173
|
|
|
181
174
|
# Collect output until marker is found or timeout
|
|
182
175
|
output_lines = []
|
|
@@ -193,23 +186,24 @@ class ShellCommunicator:
|
|
|
193
186
|
# Check if this is our completion marker
|
|
194
187
|
if marker in line:
|
|
195
188
|
marker_found = True
|
|
196
|
-
# For bash
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
189
|
+
# For bash, the exit code is on the same line after the marker
|
|
190
|
+
try:
|
|
191
|
+
# Extract exit code from the same line as the marker
|
|
192
|
+
# Format: "__COMMAND_COMPLETE_xxxxx__ exit_code"
|
|
193
|
+
parts = line.split()
|
|
194
|
+
if len(parts) >= 2:
|
|
195
|
+
exit_code_str = parts[-1].strip()
|
|
196
|
+
# Handle bash exit codes (0-255 only)
|
|
197
|
+
if exit_code_str.isdigit():
|
|
198
|
+
last_exit_code = int(exit_code_str)
|
|
199
|
+
else:
|
|
200
|
+
last_exit_code = 1
|
|
201
|
+
else:
|
|
202
|
+
last_exit_code = 1
|
|
203
|
+
except (ValueError, AttributeError, IndexError):
|
|
204
|
+
# Default to 1 if parsing fails
|
|
205
|
+
last_exit_code = 1
|
|
206
|
+
logger.debug("🔍 Found marker with exit code: %s", last_exit_code)
|
|
213
207
|
continue
|
|
214
208
|
|
|
215
209
|
# Add output to appropriate list
|
|
@@ -236,7 +230,6 @@ class ShellCommunicator:
|
|
|
236
230
|
except queue.Empty:
|
|
237
231
|
break
|
|
238
232
|
|
|
239
|
-
# TODO: Final return code is not correct. Need a fix
|
|
240
233
|
final_return_code = last_exit_code if marker_found else (1 if error_lines else 0)
|
|
241
234
|
|
|
242
235
|
# Handle timeout case
|
|
@@ -295,13 +288,8 @@ class ShellCommunicator:
|
|
|
295
288
|
|
|
296
289
|
if self.process:
|
|
297
290
|
try:
|
|
298
|
-
# Try to terminate gracefully
|
|
299
|
-
|
|
300
|
-
self.send_command("exit", wait_for_output=False)
|
|
301
|
-
elif self.shell_type == "cmd":
|
|
302
|
-
self.send_command("exit", wait_for_output=False)
|
|
303
|
-
else:
|
|
304
|
-
self.send_command("exit", wait_for_output=False)
|
|
291
|
+
# Try to terminate gracefully with bash exit command
|
|
292
|
+
self.send_command("exit", wait_for_output=False)
|
|
305
293
|
|
|
306
294
|
# Wait a bit for graceful shutdown
|
|
307
295
|
time.sleep(1)
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import logging
|
|
2
3
|
|
|
3
4
|
import uvicorn
|
|
4
5
|
from fastapi import FastAPI
|
|
5
6
|
from pydantic import BaseModel
|
|
6
7
|
from ShellCommunicator import ShellCommunicator
|
|
7
8
|
|
|
9
|
+
# Configure logging to see all logs including ShellCommunicator
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=logging.DEBUG,
|
|
12
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
13
|
+
handlers=[
|
|
14
|
+
logging.StreamHandler() # Ensure logs go to stdout
|
|
15
|
+
]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Set specific logger levels
|
|
19
|
+
logging.getLogger('ShellCommunicator').setLevel(logging.DEBUG)
|
|
20
|
+
logging.getLogger('uvicorn').setLevel(logging.INFO)
|
|
21
|
+
|
|
8
22
|
shell = ShellCommunicator("bash")
|
|
9
23
|
shell.start_session()
|
|
10
24
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class PathInfo:
|
|
8
|
+
path_valid: bool
|
|
9
|
+
base_name: str
|
|
10
|
+
abs_path: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_valid_path(path: str) -> bool:
|
|
14
|
+
try:
|
|
15
|
+
return Path(path).exists()
|
|
16
|
+
except Exception:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_absolute_path(path: str) -> bool:
|
|
21
|
+
return Path(path).is_absolute()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_base_name(path: str) -> str:
|
|
25
|
+
return Path(path).name
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_absolute_path(path: str) -> str:
|
|
29
|
+
return str(Path(path).resolve(strict=False))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_path_info(file_or_folder: str) -> PathInfo:
|
|
33
|
+
if is_valid_path(file_or_folder):
|
|
34
|
+
return PathInfo(
|
|
35
|
+
path_valid=True,
|
|
36
|
+
base_name=os.path.basename(file_or_folder),
|
|
37
|
+
abs_path=os.path.abspath(file_or_folder),
|
|
38
|
+
)
|
|
39
|
+
return PathInfo(path_valid=False, base_name="", abs_path="")
|
|
@@ -18,6 +18,7 @@ src/microbots.egg-info/requires.txt
|
|
|
18
18
|
src/microbots.egg-info/top_level.txt
|
|
19
19
|
src/microbots/bot/BrowsingBot.py
|
|
20
20
|
src/microbots/bot/CustomBot.py
|
|
21
|
+
src/microbots/bot/LogAnalysisBot.py
|
|
21
22
|
src/microbots/bot/ReadingBot.py
|
|
22
23
|
src/microbots/bot/WritingBot.py
|
|
23
24
|
src/microbots/bot/__init__.py
|
|
@@ -39,8 +40,12 @@ src/microbots/utils/logger.py
|
|
|
39
40
|
src/microbots/utils/network.py
|
|
40
41
|
src/microbots/utils/path.py
|
|
41
42
|
test/bot/browsing_bot_test.py
|
|
43
|
+
test/bot/calculator/calculator.log
|
|
44
|
+
test/bot/calculator/log_analysis_test.py
|
|
45
|
+
test/bot/calculator/code/calculator.py
|
|
42
46
|
test/bot/countries_to_capital/reading_bot_test.py
|
|
43
47
|
test/bot/countries_to_capital/writing_bot_test.py
|
|
44
48
|
test/bot/countries_to_capital/countries_dir/countries.txt
|
|
45
49
|
test/environment/local_docker/LocalDockerEnvironmentTest.py
|
|
50
|
+
test/environment/local_docker/TestFileCopy.py
|
|
46
51
|
test/environment/swe-rex/LocalDockerTest.py
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
2025-09-23 10:40:24,897 - INFO - Calculator application started.
|
|
2
|
+
2025-09-23 10:40:26,007 - INFO - User selected operation: 1
|
|
3
|
+
2025-09-23 10:40:27,391 - INFO - Numbers entered: 10.0 and 5.0
|
|
4
|
+
2025-09-23 10:40:27,391 - INFO - Result of addition: 15.0
|
|
5
|
+
2025-09-23 10:40:53,096 - INFO - Calculator application finished.
|
|
6
|
+
2025-09-23 10:41:02,897 - INFO - Calculator application started.
|
|
7
|
+
2025-09-23 10:41:03,833 - INFO - User selected operation: 1
|
|
8
|
+
2025-09-23 10:41:05,224 - INFO - Numbers entered: 10.0 and 5.0
|
|
9
|
+
2025-09-23 10:41:05,224 - INFO - Result of addition: 15.0
|
|
10
|
+
2025-09-23 10:43:52,092 - ERROR - A deliberate ZeroDivisionError was encountered.
|
|
11
|
+
Traceback (most recent call last):
|
|
12
|
+
File "C:\Users\sikannan\codeBase\minions\test\bot\calculator\code\calculator.py", line 21, in divide
|
|
13
|
+
result = a / b
|
|
14
|
+
~~^~~
|
|
15
|
+
ZeroDivisionError: division by zero
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Configure logging to write to a file named 'calculator.log'.
|
|
5
|
+
# The 'exception' level is used to capture the full traceback.
|
|
6
|
+
logging.basicConfig(
|
|
7
|
+
filename="calculator.log",
|
|
8
|
+
level=logging.ERROR,
|
|
9
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def divide(a, b):
|
|
14
|
+
"""
|
|
15
|
+
A calculator function that performs division.
|
|
16
|
+
It is intentionally designed to cause a ZeroDivisionError
|
|
17
|
+
when the divisor `b` is zero.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
logging.info(f"Attempting to divide {a} by {b}.")
|
|
21
|
+
result = a / b
|
|
22
|
+
logging.info(f"The result is: {result}")
|
|
23
|
+
return result
|
|
24
|
+
except ZeroDivisionError as e:
|
|
25
|
+
# This block catches the specific ZeroDivisionError.
|
|
26
|
+
print(
|
|
27
|
+
"An error occurred during division. Please check calculator.log for details."
|
|
28
|
+
)
|
|
29
|
+
# The logging.exception() function logs the error and the full traceback.
|
|
30
|
+
logging.exception("A deliberate ZeroDivisionError was encountered.")
|
|
31
|
+
return None
|
|
32
|
+
except Exception as e:
|
|
33
|
+
# This is a general catch-all for any other potential errors.
|
|
34
|
+
logging.error(f"An unexpected error occurred: {e}")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
# Example usage: This will work correctly.
|
|
40
|
+
print("--- First Calculation (will succeed) ---")
|
|
41
|
+
divide(10, 2)
|
|
42
|
+
print("\n")
|
|
43
|
+
|
|
44
|
+
# This call is intentionally designed to cause a ZeroDivisionError,
|
|
45
|
+
# and the error will be logged to 'calculator.log'.
|
|
46
|
+
print("--- Second Calculation (will fail on purpose) ---")
|
|
47
|
+
divide(5, 0)
|
|
48
|
+
print("\n")
|
|
49
|
+
|
|
50
|
+
print("Program finished. Check 'calculator.log' for the error details.")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
logging.basicConfig(level=logging.INFO)
|
|
8
|
+
|
|
9
|
+
# Add src directory to path to import from local source
|
|
10
|
+
sys.path.insert(
|
|
11
|
+
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../src"))
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from microbots.bot.LogAnalysisBot import LogAnalysisBot
|
|
15
|
+
from microbots.constants import DOCKER_WORKING_DIR, LOG_FILE_DIR
|
|
16
|
+
from microbots.MicroBot import BotRunResult
|
|
17
|
+
|
|
18
|
+
myBot = LogAnalysisBot(
|
|
19
|
+
model="azure-openai/mini-swe-agent-gpt5",
|
|
20
|
+
folder_to_mount=str(Path(__file__).parent / "code"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
response: BotRunResult = myBot.run(
|
|
24
|
+
str(Path(__file__).parent / "calculator.log"),
|
|
25
|
+
timeout_in_seconds=300,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
print(
|
|
29
|
+
f"Status: {response.status}\n***Result:***\n{response.result}\n===\nError: {response.error}"
|
|
30
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Simple test for file copy functionality
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import logging
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
logging.basicConfig(level=logging.INFO)
|
|
12
|
+
# Add src directory to path to import from local source
|
|
13
|
+
sys.path.insert(
|
|
14
|
+
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../src"))
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from microbots.environment.local_docker import LocalDockerEnvironment
|
|
18
|
+
|
|
19
|
+
class TestFileCopy():
|
|
20
|
+
"""Simple test for file copy"""
|
|
21
|
+
|
|
22
|
+
def test_copy_file(self):
|
|
23
|
+
"""Test copying a file to container and from container to host"""
|
|
24
|
+
# Create environment
|
|
25
|
+
env = LocalDockerEnvironment(port=8081)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Copy to container
|
|
29
|
+
# Get path to countries.txt file specifically
|
|
30
|
+
countries_file_path = Path(__file__).parent.parent.parent / "bot" / "countries_to_capital" / "countries_dir" / "countries.txt"
|
|
31
|
+
result = env.copy_to_container(str(countries_file_path), "/var/log/")
|
|
32
|
+
|
|
33
|
+
# Verify
|
|
34
|
+
print(f"Copy result: {result}")
|
|
35
|
+
if result:
|
|
36
|
+
print("✅ Copy succeeded")
|
|
37
|
+
else:
|
|
38
|
+
print("❌ Copy failed")
|
|
39
|
+
|
|
40
|
+
# Test copying from container to host
|
|
41
|
+
# Use /tmp/ which is available and writable on all systems
|
|
42
|
+
result_back = env.copy_from_container("/var/log/countries.txt", "/tmp/")
|
|
43
|
+
print(f"Copy back result: {result_back}")
|
|
44
|
+
if result_back:
|
|
45
|
+
print("✅ Copy back succeeded")
|
|
46
|
+
else:
|
|
47
|
+
print("❌ Copy back failed")
|
|
48
|
+
|
|
49
|
+
finally:
|
|
50
|
+
# Cleanup
|
|
51
|
+
# os.unlink(test_file)
|
|
52
|
+
# env.stop()
|
|
53
|
+
print("Not stopping environment for debug")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
# Run the tests
|
|
58
|
+
test_instance = TestFileCopy()
|
|
59
|
+
test_instance.test_copy_file()
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Optional
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
|
|
5
|
-
@dataclass
|
|
6
|
-
class CmdReturn:
|
|
7
|
-
stdout: str
|
|
8
|
-
stderr: str
|
|
9
|
-
return_code: int
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class Environment(ABC):
|
|
13
|
-
@abstractmethod
|
|
14
|
-
def start(self):
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
@abstractmethod
|
|
18
|
-
def stop(self):
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
@abstractmethod
|
|
22
|
-
def execute(self, command: str, timeout: Optional[int] = 300) -> CmdReturn:
|
|
23
|
-
pass
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def is_valid_path(path: str) -> bool:
|
|
6
|
-
try:
|
|
7
|
-
return Path(path).exists()
|
|
8
|
-
except Exception:
|
|
9
|
-
return False
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def is_absolute_path(path: str) -> bool:
|
|
13
|
-
return Path(path).is_absolute()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def get_base_name(path: str) -> str:
|
|
17
|
-
return Path(path).name
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def get_absolute_path(path: str) -> str:
|
|
21
|
-
return str(Path(path).resolve(strict=False))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/image_builder/Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_definitions/browser-use/browser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microbots-0.0.3 → microbots-0.0.4}/test/bot/countries_to_capital/countries_dir/countries.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microbots-0.0.3 → microbots-0.0.4}/test/environment/local_docker/LocalDockerEnvironmentTest.py
RENAMED
|
File without changes
|
|
File without changes
|