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.
Files changed (55) hide show
  1. {microbots-0.0.3 → microbots-0.0.4}/.gitignore +0 -1
  2. {microbots-0.0.3 → microbots-0.0.4}/PKG-INFO +1 -1
  3. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/MicroBot.py +3 -33
  4. microbots-0.0.4/src/microbots/bot/LogAnalysisBot.py +66 -0
  5. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/ReadingBot.py +2 -7
  6. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/WritingBot.py +2 -7
  7. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/constants.py +1 -0
  8. microbots-0.0.4/src/microbots/environment/Environment.py +35 -0
  9. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/LocalDockerEnvironment.py +132 -0
  10. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/image_builder/ShellCommunicator.py +35 -47
  11. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/image_builder/dockerShell.py +14 -0
  12. microbots-0.0.4/src/microbots/utils/path.py +39 -0
  13. {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/PKG-INFO +1 -1
  14. {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/SOURCES.txt +5 -0
  15. microbots-0.0.4/test/bot/calculator/calculator.log +15 -0
  16. microbots-0.0.4/test/bot/calculator/code/calculator.py +50 -0
  17. microbots-0.0.4/test/bot/calculator/log_analysis_test.py +30 -0
  18. microbots-0.0.4/test/environment/local_docker/TestFileCopy.py +59 -0
  19. microbots-0.0.3/src/microbots/environment/Environment.py +0 -23
  20. microbots-0.0.3/src/microbots/utils/path.py +0 -21
  21. {microbots-0.0.3 → microbots-0.0.4}/.github/workflows/dockerBuildPush.yml +0 -0
  22. {microbots-0.0.3 → microbots-0.0.4}/.github/workflows/publish.yml +0 -0
  23. {microbots-0.0.3 → microbots-0.0.4}/LICENSE +0 -0
  24. {microbots-0.0.3 → microbots-0.0.4}/README.md +0 -0
  25. {microbots-0.0.3 → microbots-0.0.4}/architechture.md +0 -0
  26. {microbots-0.0.3 → microbots-0.0.4}/docs/images/overall_architecture.png +0 -0
  27. {microbots-0.0.3 → microbots-0.0.4}/pyproject.toml +0 -0
  28. {microbots-0.0.3 → microbots-0.0.4}/pytest.ini +0 -0
  29. {microbots-0.0.3 → microbots-0.0.4}/requirements.txt +0 -0
  30. {microbots-0.0.3 → microbots-0.0.4}/setup.cfg +0 -0
  31. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/__init__.py +0 -0
  32. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/BrowsingBot.py +0 -0
  33. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/CustomBot.py +0 -0
  34. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/bot/__init__.py +0 -0
  35. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/__init__.py +0 -0
  36. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/local_docker/image_builder/Dockerfile +0 -0
  37. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/environment/swe-rex/LocalDocker.py +0 -0
  38. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/llm/__init__.py +0 -0
  39. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/llm/openai_api.py +0 -0
  40. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool.py +0 -0
  41. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_definitions/browser-use/browser.py +0 -0
  42. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_definitions/browser-use.yaml +0 -0
  43. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_definitions/cscope.yaml +0 -0
  44. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/tools/tool_test.ipynb +0 -0
  45. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/utils/logger.py +0 -0
  46. {microbots-0.0.3 → microbots-0.0.4}/src/microbots/utils/network.py +0 -0
  47. {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/dependency_links.txt +0 -0
  48. {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/requires.txt +0 -0
  49. {microbots-0.0.3 → microbots-0.0.4}/src/microbots.egg-info/top_level.txt +0 -0
  50. {microbots-0.0.3 → microbots-0.0.4}/test/bot/browsing_bot_test.py +0 -0
  51. {microbots-0.0.3 → microbots-0.0.4}/test/bot/countries_to_capital/countries_dir/countries.txt +0 -0
  52. {microbots-0.0.3 → microbots-0.0.4}/test/bot/countries_to_capital/reading_bot_test.py +0 -0
  53. {microbots-0.0.3 → microbots-0.0.4}/test/bot/countries_to_capital/writing_bot_test.py +0 -0
  54. {microbots-0.0.3 → microbots-0.0.4}/test/environment/local_docker/LocalDockerEnvironmentTest.py +0 -0
  55. {microbots-0.0.3 → microbots-0.0.4}/test/environment/swe-rex/LocalDockerTest.py +0 -0
@@ -56,7 +56,6 @@ cover/
56
56
  *.pot
57
57
 
58
58
  # Django stuff:
59
- *.log
60
59
  local_settings.py
61
60
  db.sqlite3
62
61
  db.sqlite3-journal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microbots
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: container-based autonomous agent framework
5
5
  Author-email: xxx <xxx@example.com>
6
6
  License: MIT License
@@ -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 = get_folder_mount_info(folder_to_mount)
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 = get_folder_mount_info(folder_to_mount)
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 = get_folder_mount_info(folder_to_mount)
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"""
@@ -29,4 +29,5 @@ class FILE_PERMISSIONS(IntEnum):
29
29
 
30
30
 
31
31
  DOCKER_WORKING_DIR = "workdir"
32
+ LOG_FILE_DIR = "/var/log/"
32
33
  TOOL_FILE_BASE_PATH = Path(__file__).parent / "tools" / "tool_definitions"
@@ -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
+ )
@@ -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 ("powershell", "cmd", "bash", "python")
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("&lt;", "<").replace("&gt;", ">")
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
- # Send the command
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 the marker command based on shell type
170
- if self.shell_type in ["bash", "wsl"]:
171
- self.process.stdin.write(f"echo '{marker}'; echo $? > /tmp/last_exit_code\n")
172
- elif self.shell_type == "powershell":
173
- self.process.stdin.write(f"echo '{marker}'; echo $LASTEXITCODE\n")
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/wsl, try to get the exit code from the next line
197
- if self.shell_type in ["bash", "wsl"]:
198
- try:
199
- # Try to get exit code from next output
200
- stream_type, exit_code_line = self.output_queue.get(timeout=0.5)
201
- if exit_code_line.strip().isdigit():
202
- last_exit_code = int(exit_code_line.strip())
203
- except (queue.Empty, ValueError):
204
- pass
205
- elif self.shell_type in ["powershell", "cmd"]:
206
- try:
207
- # Try to get exit code from next output
208
- stream_type, exit_code_line = self.output_queue.get(timeout=0.5)
209
- if exit_code_line.strip().isdigit():
210
- last_exit_code = int(exit_code_line.strip())
211
- except (queue.Empty, ValueError):
212
- pass
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
- if self.shell_type == "powershell":
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="")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microbots
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: container-based autonomous agent framework
5
5
  Author-email: xxx <xxx@example.com>
6
6
  License: MIT License
@@ -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