microbots 0.0.1__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.1/LICENSE +21 -0
- microbots-0.0.1/PKG-INFO +47 -0
- microbots-0.0.1/README.md +0 -0
- microbots-0.0.1/pyproject.toml +33 -0
- microbots-0.0.1/requirements.txt +9 -0
- microbots-0.0.1/setup.cfg +4 -0
- microbots-0.0.1/src/microbot/MicroBot.py +173 -0
- microbots-0.0.1/src/microbot/__init__.py +10 -0
- microbots-0.0.1/src/microbot/bot/BrowserBot.py +32 -0
- microbots-0.0.1/src/microbot/bot/CustomBot.py +31 -0
- microbots-0.0.1/src/microbot/bot/ReadingBot.py +41 -0
- microbots-0.0.1/src/microbot/bot/WritingBot.py +38 -0
- microbots-0.0.1/src/microbot/bot/__init__.py +4 -0
- microbots-0.0.1/src/microbot/constants.py +25 -0
- microbots-0.0.1/src/microbot/environment/Environment.py +22 -0
- microbots-0.0.1/src/microbot/environment/local_docker/LocalDockerEnvironment.py +156 -0
- microbots-0.0.1/src/microbot/environment/local_docker/__init__.py +1 -0
- microbots-0.0.1/src/microbot/environment/local_docker/image_builder/ShellCommunicator.py +261 -0
- microbots-0.0.1/src/microbot/environment/local_docker/image_builder/dockerShell.py +28 -0
- microbots-0.0.1/src/microbot/environment/swe-rex/LocalDocker.py +139 -0
- microbots-0.0.1/src/microbot/llm/__init__.py +0 -0
- microbots-0.0.1/src/microbot/llm/openai_api.py +78 -0
- microbots-0.0.1/src/microbot/tool/tool.py +74 -0
- microbots-0.0.1/src/microbot/tool/tool_definition/browser-use/browser.py +40 -0
- microbots-0.0.1/src/microbot/tool_definitions/base_tool.py +23 -0
- microbots-0.0.1/src/microbot/tool_definitions/ctags.py +25 -0
- microbots-0.0.1/src/microbot/tool_definitions/node.py +24 -0
- microbots-0.0.1/src/microbot/utils/logger.py +14 -0
- microbots-0.0.1/src/microbot/utils/network.py +16 -0
- microbots-0.0.1/src/microbots.egg-info/PKG-INFO +47 -0
- microbots-0.0.1/src/microbots.egg-info/SOURCES.txt +32 -0
- microbots-0.0.1/src/microbots.egg-info/dependency_links.txt +1 -0
- microbots-0.0.1/src/microbots.egg-info/requires.txt +9 -0
- microbots-0.0.1/src/microbots.egg-info/top_level.txt +1 -0
microbots-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Bala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
microbots-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microbots
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: container-based autonomous agent framework
|
|
5
|
+
Author-email: xxx <xxx@example.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Bala
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Source Repo, https://github.com/microsoft/minions
|
|
29
|
+
Project-URL: Issues, https://github.com/microsoft/minions/issues
|
|
30
|
+
Keywords: agent,bot,micro
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Requires-Python: >=3.11
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: openai==1.107.3
|
|
39
|
+
Requires-Dist: python-dotenv==1.1.1
|
|
40
|
+
Requires-Dist: docker==7.1.0
|
|
41
|
+
Requires-Dist: fastapi==0.116.1
|
|
42
|
+
Requires-Dist: uvicorn==0.35.0
|
|
43
|
+
Requires-Dist: pydantic==2.11.9
|
|
44
|
+
Requires-Dist: swe-rex==1.4.0
|
|
45
|
+
Requires-Dist: aiohttp==3.12.15
|
|
46
|
+
Requires-Dist: pyyaml==6.0.2
|
|
47
|
+
Dynamic: license-file
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "microbots" # package name on PyPI (unique)
|
|
7
|
+
dynamic = ["version", "dependencies"]
|
|
8
|
+
description = "container-based autonomous agent framework"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
keywords = ["agent", "bot", "micro"]
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "xxx", email = "xxx@example.com" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
# Indicate who your project is intended for
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.11"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.dynamic]
|
|
25
|
+
dependencies = { file = ["requirements.txt"] }
|
|
26
|
+
version = {attr ="microbot.__version__"}
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
"Source Repo" = "https://github.com/microsoft/minions"
|
|
33
|
+
Issues = "https://github.com/microsoft/minions/issues"
|
|
@@ -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
|
+
)
|
|
@@ -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
|
+
)
|
|
@@ -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
|