microbots 0.0.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.
- microbot/MicroBot.py +173 -0
- microbot/__init__.py +10 -0
- microbot/bot/BrowserBot.py +32 -0
- microbot/bot/CustomBot.py +31 -0
- microbot/bot/ReadingBot.py +41 -0
- microbot/bot/WritingBot.py +38 -0
- microbot/bot/__init__.py +4 -0
- microbot/constants.py +25 -0
- microbot/environment/Environment.py +22 -0
- microbot/environment/local_docker/LocalDockerEnvironment.py +156 -0
- microbot/environment/local_docker/__init__.py +1 -0
- microbot/environment/local_docker/image_builder/ShellCommunicator.py +261 -0
- microbot/environment/local_docker/image_builder/dockerShell.py +28 -0
- microbot/environment/swe-rex/LocalDocker.py +139 -0
- microbot/llm/__init__.py +0 -0
- microbot/llm/openai_api.py +78 -0
- microbot/tool/tool.py +74 -0
- microbot/tool/tool_definition/browser-use/browser.py +40 -0
- microbot/tool_definitions/base_tool.py +23 -0
- microbot/tool_definitions/ctags.py +25 -0
- microbot/tool_definitions/node.py +24 -0
- microbot/utils/logger.py +14 -0
- microbot/utils/network.py +16 -0
- microbots-0.0.1.dist-info/METADATA +47 -0
- microbots-0.0.1.dist-info/RECORD +28 -0
- microbots-0.0.1.dist-info/WHEEL +5 -0
- microbots-0.0.1.dist-info/licenses/LICENSE +21 -0
- microbots-0.0.1.dist-info/top_level.txt +1 -0
microbot/MicroBot.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from microbot.constants import ModelProvider, PermissionLabels, PermissionMapping
|
|
10
|
+
from microbot.environment.local_docker.LocalDockerEnvironment import LocalDockerEnvironment
|
|
11
|
+
from microbot.llm.openai_api import OpenAIApi
|
|
12
|
+
from microbot.tool_definitions.base_tool import BaseTool
|
|
13
|
+
from microbot.utils.logger import LogLevelEmoji, dividerString
|
|
14
|
+
from microbot.utils.network import get_free_port
|
|
15
|
+
|
|
16
|
+
logger = getLogger(" MicroBot ")
|
|
17
|
+
|
|
18
|
+
llm_output_format = """```json
|
|
19
|
+
{
|
|
20
|
+
task_done: true | false,
|
|
21
|
+
command: "<command to run> | null",
|
|
22
|
+
result: str | null
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
system_prompt_common = """There is a shell session open for you.
|
|
28
|
+
I will provide a task to achieve using the shell.
|
|
29
|
+
You will provide the commands to achieve the task in this particular below json format, Ensure all the time to respond in this format only and nothing else, also all the properties ( task_done, command, result ) are mandatory on each response
|
|
30
|
+
{llm_output_format}
|
|
31
|
+
after each command I will provide the output of the command.
|
|
32
|
+
ensure to run only one command at a time.
|
|
33
|
+
I won't be able to intervene once I have given task. ."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BotType(StrEnum):
|
|
37
|
+
READING_BOT = "READING_BOT"
|
|
38
|
+
WRITING_BOT = "WRITING_BOT"
|
|
39
|
+
BROWSING_BOT = "BROWSING_BOT"
|
|
40
|
+
CUSTOM_BOT = "CUSTOM_BOT"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class BotRunResult:
|
|
45
|
+
status: bool
|
|
46
|
+
result: str | None
|
|
47
|
+
error: Optional[str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MicroBot:
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
bot_type: BotType,
|
|
55
|
+
model: str,
|
|
56
|
+
system_prompt: Optional[str] = None,
|
|
57
|
+
environment: Optional[any] = None,
|
|
58
|
+
additional_tools: Optional[list[BaseTool]] = [],
|
|
59
|
+
folder_to_mount: Optional[str] = None,
|
|
60
|
+
permission: Optional[PermissionLabels] = None,
|
|
61
|
+
):
|
|
62
|
+
# validate init values before assigning
|
|
63
|
+
self.permission = permission
|
|
64
|
+
self.permission = permission
|
|
65
|
+
if folder_to_mount is not None:
|
|
66
|
+
self.folder_to_mount_base_path = os.path.basename(folder_to_mount) # TODO
|
|
67
|
+
|
|
68
|
+
self._validate_model_and_provider(model)
|
|
69
|
+
self._validate_model_and_provider(model)
|
|
70
|
+
self.permission_key = PermissionMapping.MAPPING.get(self.permission)
|
|
71
|
+
self.system_prompt = system_prompt
|
|
72
|
+
self.system_prompt = system_prompt
|
|
73
|
+
self.model = model
|
|
74
|
+
self.bot_type = bot_type
|
|
75
|
+
self.model_provider = model.split("/")[0]
|
|
76
|
+
self.deployment_name = model.split("/")[1]
|
|
77
|
+
self.environment = environment
|
|
78
|
+
self._create_environment(folder_to_mount)
|
|
79
|
+
self._create_llm()
|
|
80
|
+
|
|
81
|
+
def run(self, task, max_iterations=20, timeout_in_seconds=200) -> BotRunResult:
|
|
82
|
+
|
|
83
|
+
iteration_count = 1
|
|
84
|
+
# start timer
|
|
85
|
+
start_time = time.time()
|
|
86
|
+
timeout = timeout_in_seconds
|
|
87
|
+
llm_response = self.llm.ask(task)
|
|
88
|
+
return_value = BotRunResult(
|
|
89
|
+
status=False,
|
|
90
|
+
result=None,
|
|
91
|
+
error="Did not complete",
|
|
92
|
+
)
|
|
93
|
+
logger.info("%s TASK STARTED : %s...", LogLevelEmoji.INFO, task[0:15])
|
|
94
|
+
while llm_response.task_done is False:
|
|
95
|
+
print(dividerString)
|
|
96
|
+
logger.info(
|
|
97
|
+
" %s LLM Iteration Count : %d", LogLevelEmoji.INFO, iteration_count
|
|
98
|
+
)
|
|
99
|
+
logger.info(
|
|
100
|
+
" âĄī¸ LLM tool call : %s",
|
|
101
|
+
json.dumps(llm_response.command),
|
|
102
|
+
)
|
|
103
|
+
# increment iteration count
|
|
104
|
+
iteration_count += 1
|
|
105
|
+
if iteration_count >= max_iterations:
|
|
106
|
+
return_value.error = f"Max iterations {max_iterations} reached"
|
|
107
|
+
return return_value
|
|
108
|
+
|
|
109
|
+
# check if timeout has reached
|
|
110
|
+
current_time = time.time()
|
|
111
|
+
elapsed_time = current_time - start_time
|
|
112
|
+
|
|
113
|
+
if elapsed_time > timeout:
|
|
114
|
+
logger.error(
|
|
115
|
+
"Iteration %d with response %s",
|
|
116
|
+
iteration_count,
|
|
117
|
+
json.dumps(llm_response),
|
|
118
|
+
)
|
|
119
|
+
return_value.error = f"Timeout of {timeout} seconds reached"
|
|
120
|
+
return return_value
|
|
121
|
+
|
|
122
|
+
llm_command_output = self.environment.execute(llm_response.command)
|
|
123
|
+
logger.info(
|
|
124
|
+
" âŦ
ī¸ Command Execution Output : %s",
|
|
125
|
+
llm_command_output,
|
|
126
|
+
)
|
|
127
|
+
# Convert CmdReturn to string for LLM
|
|
128
|
+
if llm_command_output.stdout:
|
|
129
|
+
output_text = llm_command_output.stdout
|
|
130
|
+
elif llm_command_output.stderr:
|
|
131
|
+
output_text = f"COMMUNICATION ERROR: {llm_command_output.stderr}"
|
|
132
|
+
else:
|
|
133
|
+
output_text = "No output received"
|
|
134
|
+
|
|
135
|
+
llm_response = self.llm.ask(output_text)
|
|
136
|
+
|
|
137
|
+
logger.info("đ TASK COMPLETED : %s...", task[0:15])
|
|
138
|
+
return BotRunResult(status=True, result=llm_response.result, error=None)
|
|
139
|
+
|
|
140
|
+
def _create_environment(self, folder_to_mount):
|
|
141
|
+
if self.environment is None:
|
|
142
|
+
# check for a free port in the system and assign to environment
|
|
143
|
+
|
|
144
|
+
free_port = get_free_port()
|
|
145
|
+
|
|
146
|
+
self.environment = LocalDockerEnvironment(
|
|
147
|
+
port=free_port,
|
|
148
|
+
folder_to_mount=folder_to_mount,
|
|
149
|
+
permission=self.permission,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _create_llm(self):
|
|
153
|
+
if self.model_provider == ModelProvider.OPENAI:
|
|
154
|
+
self.llm = OpenAIApi(
|
|
155
|
+
system_prompt=self.system_prompt, deployment_name=self.deployment_name
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _validate_model_and_provider(self, model):
|
|
159
|
+
# Ensure it has only only slash
|
|
160
|
+
if model.count("/") != 1:
|
|
161
|
+
raise ValueError("Model should be in the format <provider>/<model_name>")
|
|
162
|
+
provider = model.split("/")[0]
|
|
163
|
+
if provider not in [e.value for e in ModelProvider]:
|
|
164
|
+
raise ValueError(f"Unsupported model provider: {provider}")
|
|
165
|
+
|
|
166
|
+
def __del__(self):
|
|
167
|
+
if self.environment:
|
|
168
|
+
try:
|
|
169
|
+
self.environment.stop()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(
|
|
172
|
+
"%s Error while stopping environment: %s", LogLevelEmoji.ERROR, e
|
|
173
|
+
)
|
microbot/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from microbot.constants import PermissionLabels
|
|
5
|
+
from microbot.MicroBot import BotType, MicroBot, system_prompt_common
|
|
6
|
+
from microbot.tool_definitions.base_tool import BaseTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BrowserBot(MicroBot):
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
model: str,
|
|
14
|
+
environment: Optional[any] = None,
|
|
15
|
+
additional_tools: Optional[list[BaseTool]] = [],
|
|
16
|
+
):
|
|
17
|
+
# validate init values before assigning
|
|
18
|
+
bot_type = BotType.BROWSING_BOT
|
|
19
|
+
permission = PermissionLabels.READ_WRITE
|
|
20
|
+
system_prompt = f"""
|
|
21
|
+
{system_prompt_common}
|
|
22
|
+
You are also provided access to internet to search for information.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
super().__init__(
|
|
26
|
+
bot_type,
|
|
27
|
+
model,
|
|
28
|
+
system_prompt,
|
|
29
|
+
environment,
|
|
30
|
+
additional_tools,
|
|
31
|
+
permission,
|
|
32
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from microbot.constants import PermissionLabels
|
|
5
|
+
from microbot.MicroBot import BotType, MicroBot
|
|
6
|
+
from microbot.tool_definitions.base_tool import BaseTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BrowserBot(MicroBot):
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
model: str,
|
|
14
|
+
system_prompt: str,
|
|
15
|
+
folder_to_mount: Optional[str] = None,
|
|
16
|
+
environment: Optional[any] = None,
|
|
17
|
+
additional_tools: Optional[list[BaseTool]] = [],
|
|
18
|
+
):
|
|
19
|
+
# validate init values before assigning
|
|
20
|
+
bot_type = BotType.BROWSING_BOT
|
|
21
|
+
permission = PermissionLabels.READ_WRITE
|
|
22
|
+
|
|
23
|
+
super().__init__(
|
|
24
|
+
bot_type,
|
|
25
|
+
model,
|
|
26
|
+
system_prompt,
|
|
27
|
+
environment,
|
|
28
|
+
additional_tools,
|
|
29
|
+
folder_to_mount,
|
|
30
|
+
permission,
|
|
31
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from microbot.constants import DOCKER_WORKING_DIR, PermissionLabels
|
|
5
|
+
from microbot.MicroBot import BotType, MicroBot, system_prompt_common
|
|
6
|
+
from microbot.tool_definitions.base_tool import BaseTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ReadingBot(MicroBot):
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
model: str,
|
|
14
|
+
folder_to_mount: str,
|
|
15
|
+
environment: Optional[any] = None,
|
|
16
|
+
additional_tools: Optional[list[BaseTool]] = [],
|
|
17
|
+
):
|
|
18
|
+
# validate init values before assigning
|
|
19
|
+
bot_type = BotType.READING_BOT
|
|
20
|
+
permission = PermissionLabels.READ_ONLY
|
|
21
|
+
|
|
22
|
+
base_name = Path(folder_to_mount).name
|
|
23
|
+
|
|
24
|
+
system_prompt = f"""
|
|
25
|
+
{system_prompt_common}
|
|
26
|
+
You are a reading bot.
|
|
27
|
+
You are only provided access to read files inside the mounted directory.
|
|
28
|
+
The directory is mounted at /{DOCKER_WORKING_DIR}/{base_name} in your current environment.
|
|
29
|
+
You can access files using paths like /{DOCKER_WORKING_DIR}/{base_name}/filename.txt or by changing to that directory first.
|
|
30
|
+
Once all the commands are done, and task is verified finally give me the result.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
super().__init__(
|
|
34
|
+
bot_type,
|
|
35
|
+
model,
|
|
36
|
+
system_prompt,
|
|
37
|
+
environment,
|
|
38
|
+
additional_tools,
|
|
39
|
+
folder_to_mount,
|
|
40
|
+
permission,
|
|
41
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from microbot.constants import PermissionLabels
|
|
4
|
+
from microbot.MicroBot import BotType, MicroBot, system_prompt_common
|
|
5
|
+
from microbot.tool_definitions.base_tool import BaseTool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WritingBot(MicroBot):
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
model: str,
|
|
13
|
+
folder_to_mount: str,
|
|
14
|
+
environment: Optional[any] = None,
|
|
15
|
+
additional_tools: Optional[list[BaseTool]] = [],
|
|
16
|
+
):
|
|
17
|
+
# validate init values before assigning
|
|
18
|
+
bot_type = BotType.WRITING_BOT
|
|
19
|
+
permission = PermissionLabels.READ_WRITE
|
|
20
|
+
|
|
21
|
+
system_prompt = f"""
|
|
22
|
+
{system_prompt_common}
|
|
23
|
+
You are a writing bot.
|
|
24
|
+
You are only provided access to write files inside the mounted directory.
|
|
25
|
+
The directory is mounted at /app/{folder_to_mount} in your current environment.
|
|
26
|
+
You can access files using paths like /app/{folder_to_mount}/filename.txt or by changing to that directory first.
|
|
27
|
+
Once all the commands are done, and task is verified finally give me the result.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
super().__init__(
|
|
31
|
+
bot_type,
|
|
32
|
+
model,
|
|
33
|
+
system_prompt,
|
|
34
|
+
environment,
|
|
35
|
+
additional_tools,
|
|
36
|
+
folder_to_mount,
|
|
37
|
+
permission,
|
|
38
|
+
)
|
microbot/bot/__init__.py
ADDED
microbot/constants.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from enum import Enum, StrEnum
|
|
2
|
+
from typing import Optional, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ModelProvider(StrEnum):
|
|
6
|
+
OPENAI = "openai"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelEnum(StrEnum):
|
|
10
|
+
GPT_5 = "gpt-5"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PermissionLabels(StrEnum):
|
|
14
|
+
READ_ONLY = "READ_ONLY"
|
|
15
|
+
READ_WRITE = "READ_WRITE"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PermissionMapping:
|
|
19
|
+
MAPPING = {
|
|
20
|
+
PermissionLabels.READ_ONLY: "ro",
|
|
21
|
+
PermissionLabels.READ_WRITE: "rw",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
DOCKER_WORKING_DIR = "workdir"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
class CmdReturn:
|
|
5
|
+
def __init__(self, stdout: str, stderr: str, return_code: int):
|
|
6
|
+
self.stdout = stdout
|
|
7
|
+
self.stderr = stderr
|
|
8
|
+
self.return_code = return_code
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Environment(ABC):
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def start(self):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def stop(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def execute(self, command: str, timeout: Optional[int] = 300) -> CmdReturn:
|
|
22
|
+
pass
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from microbot.environment.Environment import Environment, CmdReturn
|
|
8
|
+
|
|
9
|
+
import docker
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
WORKING_DIR = str(Path.home() / "MICROBOT_WORKDIR")
|
|
15
|
+
DOCKER_WORKING_DIR = "/workdir"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LocalDockerEnvironment(Environment):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
port: int,
|
|
22
|
+
folder_to_mount: Optional[str] = None,
|
|
23
|
+
permission: Optional[str] = None,
|
|
24
|
+
image: str = "kavyasree261002/shell_server:latest",
|
|
25
|
+
):
|
|
26
|
+
if folder_to_mount is None and permission is not None:
|
|
27
|
+
raise ValueError("permission provided but folder_to_mount is None")
|
|
28
|
+
elif permission is None and folder_to_mount is not None:
|
|
29
|
+
raise ValueError("folder_to_mount provided but permission is None")
|
|
30
|
+
if permission is not None and permission not in ["READ_ONLY", "READ_WRITE"]:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"permission must be 'READ_ONLY' or 'READ_WRITE' when provided"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
self.image = image
|
|
36
|
+
self.folder_to_mount = folder_to_mount
|
|
37
|
+
self.permission = permission
|
|
38
|
+
self.container = None
|
|
39
|
+
self.client = docker.from_env()
|
|
40
|
+
self.port = port # required host port
|
|
41
|
+
self.container_port = 8080
|
|
42
|
+
self._create_working_dir()
|
|
43
|
+
self.start()
|
|
44
|
+
|
|
45
|
+
def _create_working_dir(self):
|
|
46
|
+
if not os.path.exists(WORKING_DIR):
|
|
47
|
+
os.makedirs(WORKING_DIR)
|
|
48
|
+
logger.info("đī¸ Created working directory at %s", WORKING_DIR)
|
|
49
|
+
else:
|
|
50
|
+
logger.info("đī¸ Working directory already exists at %s", WORKING_DIR)
|
|
51
|
+
|
|
52
|
+
def start(self):
|
|
53
|
+
mode_map = {"READ_ONLY": "ro", "READ_WRITE": "rw"}
|
|
54
|
+
volumes_config = {WORKING_DIR: {"bind": DOCKER_WORKING_DIR, "mode": "rw"}}
|
|
55
|
+
if self.folder_to_mount and self.permission:
|
|
56
|
+
if self.permission == "READ_ONLY":
|
|
57
|
+
volumes_config[self.folder_to_mount] = {
|
|
58
|
+
"bind": f"/ro/{os.path.basename(self.folder_to_mount)}",
|
|
59
|
+
"mode": mode_map[self.permission],
|
|
60
|
+
}
|
|
61
|
+
logger.info(
|
|
62
|
+
"đĻ Volume mapping: %s â /ro/%s",
|
|
63
|
+
self.folder_to_mount,
|
|
64
|
+
os.path.basename(self.folder_to_mount),
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
volumes_config[self.folder_to_mount] = {
|
|
68
|
+
"bind": f"/{DOCKER_WORKING_DIR}/{os.path.basename(self.folder_to_mount)}",
|
|
69
|
+
"mode": mode_map[self.permission],
|
|
70
|
+
}
|
|
71
|
+
logger.debug(
|
|
72
|
+
"đĻ Volume mapping: %s â /{DOCKER_WORKING_DIR}/%s",
|
|
73
|
+
self.folder_to_mount,
|
|
74
|
+
os.path.basename(self.folder_to_mount),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Port mapping
|
|
78
|
+
port_mapping = {f"{self.container_port}/tcp": self.port}
|
|
79
|
+
|
|
80
|
+
self.container = self.client.containers.run(
|
|
81
|
+
self.image,
|
|
82
|
+
volumes=volumes_config,
|
|
83
|
+
ports=port_mapping,
|
|
84
|
+
detach=True,
|
|
85
|
+
working_dir="/app",
|
|
86
|
+
privileged=True, # Required for mounting overlayfs
|
|
87
|
+
environment={"BOT_PORT": str(self.container_port)},
|
|
88
|
+
)
|
|
89
|
+
logger.info(
|
|
90
|
+
"đ Started container %s with image %s on host port %s",
|
|
91
|
+
self.container.id[:12],
|
|
92
|
+
self.image,
|
|
93
|
+
self.port,
|
|
94
|
+
)
|
|
95
|
+
time.sleep(2) # Give some time for the server to start
|
|
96
|
+
|
|
97
|
+
if self.permission == "READ_ONLY":
|
|
98
|
+
self._setup_overlay_mount(self.folder_to_mount)
|
|
99
|
+
|
|
100
|
+
def _setup_overlay_mount(self, folder_to_mount: str):
|
|
101
|
+
path_name = os.path.basename(os.path.abspath(folder_to_mount))
|
|
102
|
+
# Mount /ro/path_name to /{WORKING_DIR}/path_name using overlayfs
|
|
103
|
+
mount_command = (
|
|
104
|
+
f"mkdir -p /overlaydir && "
|
|
105
|
+
f"mkdir -p /{DOCKER_WORKING_DIR}/{path_name} /{DOCKER_WORKING_DIR}/overlay/{path_name}/upper /{DOCKER_WORKING_DIR}/overlay/{path_name}/work && "
|
|
106
|
+
f"mount -t overlay overlay -o lowerdir=/ro/{path_name},upperdir=/{DOCKER_WORKING_DIR}/overlay/{path_name}/upper,workdir=/{DOCKER_WORKING_DIR}/overlay/{path_name}/work /{DOCKER_WORKING_DIR}/{path_name}"
|
|
107
|
+
)
|
|
108
|
+
self.execute(mount_command)
|
|
109
|
+
logger.info(
|
|
110
|
+
"đ Set up overlay mount for read-only directory at /{DOCKER_WORKING_DIR}/%s",
|
|
111
|
+
path_name,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def stop(self):
|
|
115
|
+
"""Stop and remove the container"""
|
|
116
|
+
if self.container:
|
|
117
|
+
self.container.stop()
|
|
118
|
+
self.container.remove()
|
|
119
|
+
self.container = None
|
|
120
|
+
|
|
121
|
+
# Remove working directory
|
|
122
|
+
if os.path.exists(WORKING_DIR):
|
|
123
|
+
try:
|
|
124
|
+
import shutil
|
|
125
|
+
|
|
126
|
+
shutil.rmtree(WORKING_DIR)
|
|
127
|
+
logger.info("đī¸ Removed working directory at %s", WORKING_DIR)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error("â Failed to remove working directory: %s", e)
|
|
130
|
+
|
|
131
|
+
def execute(self, command: str, timeout: Optional[int] = 10) -> CmdReturn: # TODO: Need proper return value
|
|
132
|
+
logger.debug("âĄī¸ Executing command in container: %s", command)
|
|
133
|
+
try:
|
|
134
|
+
response = requests.post(
|
|
135
|
+
f"http://localhost:{self.port}/",
|
|
136
|
+
json={"message": command},
|
|
137
|
+
timeout=timeout,
|
|
138
|
+
)
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
logger.debug("âŦ
ī¸ Command output: %s", response.json().get("output", ""))
|
|
141
|
+
output = response.json().get("output", "")
|
|
142
|
+
return CmdReturn(
|
|
143
|
+
stdout=output, stderr="", return_code=0
|
|
144
|
+
)
|
|
145
|
+
self.container.reload()
|
|
146
|
+
logger.info("âšī¸ Container status: %s", self.container.status)
|
|
147
|
+
if self.container.status != "running":
|
|
148
|
+
logs = self.container.logs().decode("utf-8", errors="replace")
|
|
149
|
+
logger.error("đ Container not running. Recent logs below:\n%s", logs)
|
|
150
|
+
return CmdReturn(stdout="", stderr="Connection error", return_code=1)
|
|
151
|
+
except requests.exceptions.RequestException as e:
|
|
152
|
+
logger.exception("â Request failed while executing command: %s", e)
|
|
153
|
+
return CmdReturn(stdout="", stderr=str(e), return_code=1)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.exception("â Unexpected error while executing command: %s", e)
|
|
156
|
+
return CmdReturn(stdout="", stderr="Unexpected error", return_code=1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .LocalDockerEnvironment import LocalDockerEnvironment
|