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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shell Communication Script
|
|
4
|
+
A Python script to create and communicate with shell sessions.
|
|
5
|
+
Supports interactive shell communication, command execution, and bidirectional data flow.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import queue
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Callable, List, Optional
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CmdReturn:
|
|
22
|
+
stdout: str
|
|
23
|
+
stderr: str
|
|
24
|
+
return_code: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ShellCommunicator:
|
|
28
|
+
"""
|
|
29
|
+
A class to create and manage shell sessions with bidirectional communication.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, shell_type: str = "bash", encoding: str = "utf-8"):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the shell communicator.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
shell_type: Type of shell ("powershell", "cmd", "bash", "python")
|
|
38
|
+
encoding: Text encoding for communication
|
|
39
|
+
"""
|
|
40
|
+
self.shell_type = shell_type.lower()
|
|
41
|
+
self.encoding = encoding
|
|
42
|
+
self.process: Optional[subprocess.Popen] = None
|
|
43
|
+
self.output_queue = queue.Queue()
|
|
44
|
+
self.error_queue = queue.Queue()
|
|
45
|
+
self.is_running = False
|
|
46
|
+
self.output_thread: Optional[threading.Thread] = None
|
|
47
|
+
self.error_thread: Optional[threading.Thread] = None
|
|
48
|
+
self.output_callback: Optional[Callable] = None
|
|
49
|
+
|
|
50
|
+
# Define shell commands
|
|
51
|
+
self.shell_commands = {
|
|
52
|
+
"powershell": ["powershell.exe", "-NoLogo", "-NoExit"],
|
|
53
|
+
"cmd": ["cmd.exe", "/k"],
|
|
54
|
+
"bash": ["bash"],
|
|
55
|
+
"python": [sys.executable, "-i"],
|
|
56
|
+
"wsl": ["wsl.exe"],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def start_session(self) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Start a new shell session.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
bool: True if session started successfully, False otherwise
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
if self.shell_type not in self.shell_commands:
|
|
68
|
+
logger.error("🛑 Unsupported shell type: %s", self.shell_type)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
cmd = self.shell_commands[self.shell_type]
|
|
72
|
+
|
|
73
|
+
# Create the subprocess
|
|
74
|
+
self.process = subprocess.Popen(
|
|
75
|
+
cmd,
|
|
76
|
+
stdin=subprocess.PIPE,
|
|
77
|
+
stdout=subprocess.PIPE,
|
|
78
|
+
stderr=subprocess.PIPE,
|
|
79
|
+
text=True,
|
|
80
|
+
encoding=self.encoding,
|
|
81
|
+
bufsize=0,
|
|
82
|
+
universal_newlines=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.is_running = True
|
|
86
|
+
|
|
87
|
+
# Start output monitoring threads
|
|
88
|
+
self.output_thread = threading.Thread(
|
|
89
|
+
target=self._monitor_output,
|
|
90
|
+
args=(self.process.stdout, self.output_queue, "OUTPUT"),
|
|
91
|
+
daemon=True,
|
|
92
|
+
)
|
|
93
|
+
self.error_thread = threading.Thread(
|
|
94
|
+
target=self._monitor_output,
|
|
95
|
+
args=(self.process.stderr, self.error_queue, "ERROR"),
|
|
96
|
+
daemon=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.output_thread.start()
|
|
100
|
+
self.error_thread.start()
|
|
101
|
+
|
|
102
|
+
logger.info("🚀 %s session started successfully", self.shell_type.capitalize())
|
|
103
|
+
logger.debug("🆔 Process ID: %s", self.process.pid)
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.exception("❌ Failed to start shell session: %s", e)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def _monitor_output(self, stream, output_queue: queue.Queue, stream_type: str):
|
|
111
|
+
"""
|
|
112
|
+
Monitor shell output in a separate thread.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
stream: The stream to monitor (stdout or stderr)
|
|
116
|
+
output_queue: Queue to store output
|
|
117
|
+
stream_type: Type of stream ("OUTPUT" or "ERROR")
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
while self.is_running and self.process and self.process.poll() is None:
|
|
121
|
+
line = stream.readline()
|
|
122
|
+
if line:
|
|
123
|
+
output_queue.put((stream_type, line.rstrip()))
|
|
124
|
+
if self.output_callback:
|
|
125
|
+
self.output_callback(stream_type, line.rstrip())
|
|
126
|
+
elif self.process.poll() is not None:
|
|
127
|
+
break
|
|
128
|
+
except Exception as e:
|
|
129
|
+
output_queue.put((stream_type, f"Monitor error: {e}"))
|
|
130
|
+
|
|
131
|
+
def send_command(
|
|
132
|
+
self, command: str, wait_for_output: bool = True, timeout: float = 5.0
|
|
133
|
+
) -> CmdReturn:
|
|
134
|
+
"""
|
|
135
|
+
Send a command to the shell session.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
command: Command to execute
|
|
139
|
+
wait_for_output: Whether to wait for command output
|
|
140
|
+
timeout: Timeout for waiting for output
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of output lines
|
|
144
|
+
"""
|
|
145
|
+
if not self.is_running or not self.process:
|
|
146
|
+
logger.warning("⚠️ No active shell session")
|
|
147
|
+
return CmdReturn(stdout="", stderr="No active shell session", return_code=1)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
self.process.stdin.write(command + "\n")
|
|
151
|
+
self.process.stdin.flush()
|
|
152
|
+
logger.debug("➡️ Sent command: %s", command)
|
|
153
|
+
|
|
154
|
+
if not wait_for_output:
|
|
155
|
+
return CmdReturn(stdout="", stderr="", return_code=0)
|
|
156
|
+
|
|
157
|
+
output_lines = []
|
|
158
|
+
start_time = time.time()
|
|
159
|
+
|
|
160
|
+
while time.time() - start_time < timeout:
|
|
161
|
+
try:
|
|
162
|
+
# Check for output
|
|
163
|
+
stream_type, line = self.output_queue.get(timeout=0.1)
|
|
164
|
+
output_lines.append(f"{line}")
|
|
165
|
+
if stream_type == "ERROR":
|
|
166
|
+
logger.error("❌ %s", line)
|
|
167
|
+
else:
|
|
168
|
+
logger.debug("📤 %s", line)
|
|
169
|
+
except queue.Empty:
|
|
170
|
+
continue
|
|
171
|
+
except Exception:
|
|
172
|
+
logger.exception("❌ Unexpected error while reading output queue")
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
# Check for errors
|
|
176
|
+
try:
|
|
177
|
+
while True:
|
|
178
|
+
stream_type, line = self.error_queue.get_nowait()
|
|
179
|
+
output_lines.append(f"{line}")
|
|
180
|
+
if stream_type == "ERROR":
|
|
181
|
+
logger.error("❌ %s", line)
|
|
182
|
+
else:
|
|
183
|
+
logger.debug("📤 %s", line)
|
|
184
|
+
except queue.Empty:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
return CmdReturn(stdout="\n".join(output_lines), stderr="", return_code=0)
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.exception("❌ Failed to send command: %s", e)
|
|
191
|
+
return CmdReturn(stdout="", stderr=str(e), return_code=1)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def is_alive(self) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
Check if the shell session is still alive.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
bool: True if session is active, False otherwise
|
|
200
|
+
"""
|
|
201
|
+
return (
|
|
202
|
+
self.is_running and self.process is not None and self.process.poll() is None
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def get_shell_info(self) -> dict:
|
|
206
|
+
"""
|
|
207
|
+
Get information about the current shell session.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dictionary with shell session information
|
|
211
|
+
"""
|
|
212
|
+
if not self.process:
|
|
213
|
+
return {"status": "Not started"}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"shell_type": self.shell_type,
|
|
217
|
+
"pid": self.process.pid,
|
|
218
|
+
"status": "Running" if self.is_alive() else "Stopped",
|
|
219
|
+
"encoding": self.encoding,
|
|
220
|
+
"return_code": self.process.returncode,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
def close_session(self):
|
|
224
|
+
"""
|
|
225
|
+
Close the shell session and cleanup resources.
|
|
226
|
+
"""
|
|
227
|
+
logger.info("🛑 Closing shell session…")
|
|
228
|
+
|
|
229
|
+
self.is_running = False
|
|
230
|
+
|
|
231
|
+
if self.process:
|
|
232
|
+
try:
|
|
233
|
+
# Try to terminate gracefully
|
|
234
|
+
if self.shell_type == "powershell":
|
|
235
|
+
self.send_command("exit", wait_for_output=False)
|
|
236
|
+
elif self.shell_type == "cmd":
|
|
237
|
+
self.send_command("exit", wait_for_output=False)
|
|
238
|
+
else:
|
|
239
|
+
self.send_command("exit", wait_for_output=False)
|
|
240
|
+
|
|
241
|
+
# Wait a bit for graceful shutdown
|
|
242
|
+
time.sleep(1)
|
|
243
|
+
|
|
244
|
+
# Force terminate if still running
|
|
245
|
+
if self.process.poll() is None:
|
|
246
|
+
self.process.terminate()
|
|
247
|
+
time.sleep(1)
|
|
248
|
+
|
|
249
|
+
if self.process.poll() is None:
|
|
250
|
+
self.process.kill()
|
|
251
|
+
|
|
252
|
+
logger.info("✅ Shell session closed")
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.exception("⚠️ Error during cleanup: %s", e)
|
|
256
|
+
|
|
257
|
+
# Wait for threads to finish
|
|
258
|
+
if self.output_thread and self.output_thread.is_alive():
|
|
259
|
+
self.output_thread.join(timeout=2)
|
|
260
|
+
if self.error_thread and self.error_thread.is_alive():
|
|
261
|
+
self.error_thread.join(timeout=2)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import uvicorn
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from ShellCommunicator import ShellCommunicator
|
|
7
|
+
|
|
8
|
+
shell = ShellCommunicator("bash")
|
|
9
|
+
shell.start_session()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Message(BaseModel):
|
|
13
|
+
message: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
app = FastAPI()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.post("/")
|
|
20
|
+
async def receive_message(message: Message):
|
|
21
|
+
command_output = shell.send_command(message.message)
|
|
22
|
+
return {"status": "success", "output": "\n".join(command_output)}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
# Prefer BOT_PORT, else default 8080
|
|
27
|
+
port = int(os.getenv("BOT_PORT") or 8080)
|
|
28
|
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional, Final
|
|
6
|
+
|
|
7
|
+
from Environment.Environment import Environment, CmdReturn
|
|
8
|
+
from swerex.deployment.docker import DockerDeployment
|
|
9
|
+
from swerex.runtime.abstract import (
|
|
10
|
+
CreateBashSessionRequest,
|
|
11
|
+
CloseBashSessionRequest,
|
|
12
|
+
BashAction,
|
|
13
|
+
Observation,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
PYTHON_IMAGE = "mcr.microsoft.com/devcontainers/python:3.11"
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Permission(str, Enum):
|
|
22
|
+
READ_ONLY = "READ_ONLY"
|
|
23
|
+
READ_WRITE = "READ_WRITE"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LocalDocker(Environment):
|
|
27
|
+
BASE_PATH: Final[str] = "/workdir"
|
|
28
|
+
|
|
29
|
+
def _validate_permission_args(
|
|
30
|
+
self,
|
|
31
|
+
folder_to_mount: Optional[str],
|
|
32
|
+
permission: Optional[Permission],
|
|
33
|
+
):
|
|
34
|
+
if folder_to_mount is None and permission is not None:
|
|
35
|
+
raise ValueError("permission provided but folder_to_mount is None")
|
|
36
|
+
if permission is None and folder_to_mount is not None:
|
|
37
|
+
raise ValueError("folder_to_mount provided but permission is None")
|
|
38
|
+
if permission is not None and permission not in (
|
|
39
|
+
Permission.READ_ONLY,
|
|
40
|
+
Permission.READ_WRITE,
|
|
41
|
+
):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"permission must be Permission.READ_ONLY or Permission.READ_WRITE when provided"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _get_mount_args(
|
|
47
|
+
self,
|
|
48
|
+
folder_to_mount: Optional[str],
|
|
49
|
+
permission: Optional[Permission],
|
|
50
|
+
) -> str:
|
|
51
|
+
self._validate_permission_args(folder_to_mount, permission)
|
|
52
|
+
|
|
53
|
+
mount_args = ""
|
|
54
|
+
if folder_to_mount and permission:
|
|
55
|
+
# TODO: Make overlay mount for read-only to allow llm to create intermediate files
|
|
56
|
+
sanitized = os.path.abspath(folder_to_mount).strip()
|
|
57
|
+
target_path = f"{LocalDocker.BASE_PATH}/{os.path.basename(sanitized)}"
|
|
58
|
+
mode = "ro" if permission == Permission.READ_ONLY else "rw"
|
|
59
|
+
mount_args = f"-v {sanitized}:{target_path}:{mode}"
|
|
60
|
+
logger.info("🥪 Volume mapping: %s -> %s (%s)", sanitized, target_path, mode)
|
|
61
|
+
logger.debug("🗻 Mount args: %r", mount_args)
|
|
62
|
+
return mount_args
|
|
63
|
+
|
|
64
|
+
def _get_docker_args(self, mount_args: str = "") -> list[str]:
|
|
65
|
+
# _get_mount_args returns either "" or a single string beginning with -v; split into tokens for DockerDeployment
|
|
66
|
+
docker_args = []
|
|
67
|
+
|
|
68
|
+
if mount_args:
|
|
69
|
+
if mount_args.startswith('-v '):
|
|
70
|
+
# split once: '-v src:dest:mode'
|
|
71
|
+
flag, rest = mount_args.split(' ', 1)
|
|
72
|
+
docker_args.extend([flag, rest])
|
|
73
|
+
else:
|
|
74
|
+
docker_args.append(mount_args)
|
|
75
|
+
|
|
76
|
+
return docker_args
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
folder_to_mount: Optional[str] = None,
|
|
81
|
+
permission: Optional[Permission] = Permission.READ_WRITE,
|
|
82
|
+
image: str = PYTHON_IMAGE,
|
|
83
|
+
):
|
|
84
|
+
mount_args = self._get_mount_args(folder_to_mount, permission)
|
|
85
|
+
docker_args = self._get_docker_args(mount_args)
|
|
86
|
+
self.deployment = DockerDeployment(image=image, docker_args=docker_args)
|
|
87
|
+
asyncio.run(self.deployment.start())
|
|
88
|
+
self.start()
|
|
89
|
+
logger.info("🚀 LocalDocker environment initialized successfully")
|
|
90
|
+
|
|
91
|
+
def start(self): # type: ignore[override]
|
|
92
|
+
# Acquire runtime and open a bash session.
|
|
93
|
+
self.runtime = self.deployment.runtime
|
|
94
|
+
asyncio.run(self.runtime.create_session(CreateBashSessionRequest()))
|
|
95
|
+
|
|
96
|
+
def stop(self):
|
|
97
|
+
try:
|
|
98
|
+
asyncio.run(self.runtime.close_session(CloseBashSessionRequest()))
|
|
99
|
+
finally:
|
|
100
|
+
asyncio.run(self.deployment.stop())
|
|
101
|
+
|
|
102
|
+
async def execute(
|
|
103
|
+
self, command: str, timeout: Optional[int] = 300
|
|
104
|
+
) -> CmdReturn:
|
|
105
|
+
"""Execute a shell command inside the container.
|
|
106
|
+
|
|
107
|
+
We pass the command through bash -lc to support shell features (globbing, env vars, pipelines).
|
|
108
|
+
"""
|
|
109
|
+
logger.debug("🔧 Executing command: %s", command)
|
|
110
|
+
try:
|
|
111
|
+
output: Observation = await asyncio.wait_for(
|
|
112
|
+
self.runtime.run_in_session(BashAction(command=command)), timeout
|
|
113
|
+
)
|
|
114
|
+
logger.debug("📋 Command '%s' completed:", command)
|
|
115
|
+
logger.debug(" ├─ 📤 Exit code: %s", output.exit_code)
|
|
116
|
+
logger.debug(" ├─ 📝 Output: %s", output.output[:100] + "..." if len(output.output) > 100 else output.output)
|
|
117
|
+
logger.debug(" └─ ⚠️ Error: %s", output.failure_reason if output.failure_reason else "(none)")
|
|
118
|
+
return CmdReturn(
|
|
119
|
+
stdout=output.output,
|
|
120
|
+
return_code=output.exit_code,
|
|
121
|
+
stderr=output.failure_reason if output.failure_reason else "",
|
|
122
|
+
)
|
|
123
|
+
except asyncio.TimeoutError:
|
|
124
|
+
# TODO: Consider killing the process if it exceeds timeout. Because session might be unusable after timeout.
|
|
125
|
+
logger.error("⏱️ Command timed out after %s seconds: '%s'", timeout, command)
|
|
126
|
+
return CmdReturn(
|
|
127
|
+
stdout="",
|
|
128
|
+
stderr=f"Command timed out after {timeout} seconds",
|
|
129
|
+
return_code=124, # Standard timeout exit code
|
|
130
|
+
)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error("❌ Error occurred while executing command '%s': %s", command, e)
|
|
133
|
+
return CmdReturn(
|
|
134
|
+
stdout="",
|
|
135
|
+
stderr=str(e),
|
|
136
|
+
return_code=1,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
microbot/llm/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
from openai import OpenAI
|
|
8
|
+
from microbot.utils.logger import LogLevelEmoji
|
|
9
|
+
|
|
10
|
+
logger = getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
from openai import OpenAI
|
|
16
|
+
|
|
17
|
+
endpoint = os.getenv("OPEN_AI_END_POINT")
|
|
18
|
+
deployment_name = os.getenv("OPEN_AI_DEPLOYMENT_NAME")
|
|
19
|
+
api_key = os.getenv("OPEN_AI_KEY") # use the api_key
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class llmAskResponse:
|
|
24
|
+
task_done: bool = False
|
|
25
|
+
command: str = ""
|
|
26
|
+
result: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OpenAIApi:
|
|
30
|
+
|
|
31
|
+
def __init__(self, system_prompt, deployment_name=deployment_name):
|
|
32
|
+
self.ai_client = OpenAI(base_url=f"{endpoint}", api_key=api_key)
|
|
33
|
+
self.deployment_name = deployment_name
|
|
34
|
+
self.system_prompt = system_prompt
|
|
35
|
+
self.messages = [{"role": "system", "content": system_prompt}]
|
|
36
|
+
|
|
37
|
+
def ask(self, message) -> llmAskResponse:
|
|
38
|
+
self.messages.append({"role": "user", "content": message})
|
|
39
|
+
return_value = {}
|
|
40
|
+
while self._validate_llm_response(return_value) is False:
|
|
41
|
+
response = self.ai_client.responses.create(
|
|
42
|
+
model=self.deployment_name,
|
|
43
|
+
input=self.messages,
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
return_value = json.loads(response.output_text)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(
|
|
49
|
+
f"%s Error occurred while dumping JSON: {e}", LogLevelEmoji.ERROR
|
|
50
|
+
)
|
|
51
|
+
logger.error(
|
|
52
|
+
"%s Failed to parse JSON from LLM response and the response is",
|
|
53
|
+
LogLevelEmoji.ERROR,
|
|
54
|
+
)
|
|
55
|
+
logger.error(response.output_text)
|
|
56
|
+
|
|
57
|
+
self.messages.append({"role": "assistant", "content": json.dumps(return_value)})
|
|
58
|
+
|
|
59
|
+
return llmAskResponse(
|
|
60
|
+
task_done=return_value["task_done"],
|
|
61
|
+
result=return_value["result"],
|
|
62
|
+
command=return_value["command"],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def clear_history(self):
|
|
66
|
+
self.messages = [
|
|
67
|
+
{
|
|
68
|
+
"role": "system",
|
|
69
|
+
"content": self.system_prompt,
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
def _validate_llm_response(self, response: dict) -> bool:
|
|
75
|
+
if "task_done" in response and "command" in response and "result" in response:
|
|
76
|
+
logger.info("The llm response is %s ", response)
|
|
77
|
+
return True
|
|
78
|
+
return False
|
microbot/tool/tool.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FILE_PERMISSION(IntEnum):
|
|
9
|
+
READ = 4
|
|
10
|
+
WRITE = 2
|
|
11
|
+
EXECUTE = 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class EnvFileCopies:
|
|
16
|
+
src: Path
|
|
17
|
+
dest: Path
|
|
18
|
+
permissions: int # Use FILE_PERMISSION enum to set permissions
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Tool:
|
|
23
|
+
# TODO: Handle different instructions based on the platform (linux flavours, windows, mac)
|
|
24
|
+
# TODO: Add versioning to tools
|
|
25
|
+
name: str
|
|
26
|
+
description: str
|
|
27
|
+
parameters: dict | None
|
|
28
|
+
|
|
29
|
+
# This is the set of instructions that will be provided to the LLM on how to use this tool.
|
|
30
|
+
# This string will be appended to the LLM's system prompt.
|
|
31
|
+
# This instructions should be non-interactive
|
|
32
|
+
usage_instructions_to_llm: str
|
|
33
|
+
|
|
34
|
+
# This set of commands will be executed once the environment is up and running.
|
|
35
|
+
# These commands will be executed in the order they are provided.
|
|
36
|
+
install_commands: List[str]
|
|
37
|
+
|
|
38
|
+
# Mention what are the environment variables that need to be copied from your current environment
|
|
39
|
+
env_variables: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
# Any files to be copied to the environment before the tool is installed.
|
|
42
|
+
files_to_copy: Optional[List[EnvFileCopies]] = None
|
|
43
|
+
|
|
44
|
+
# This set of commands will be executed to verify if the tool is installed correctly.
|
|
45
|
+
# If any of these commands fail, the tool installation is considered to have failed.
|
|
46
|
+
verify_commands: Optional[List[str]] = None
|
|
47
|
+
|
|
48
|
+
# This set of commands will be executed after the code is copied to the environment
|
|
49
|
+
# and before the llm is invoked.
|
|
50
|
+
# These commands will be executed inside the mounted folder.
|
|
51
|
+
setup_commands: Optional[List[str]] = None
|
|
52
|
+
|
|
53
|
+
# This set of commands will be executed when the environment is being torn down.
|
|
54
|
+
uninstall_commands: Optional[List[str]] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_tool_definition(yaml_path: Path) -> Tool:
|
|
58
|
+
"""
|
|
59
|
+
Parse a tool definition from a YAML file.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
yaml_path: The path to the YAML file containing the tool definition.
|
|
63
|
+
If it is not an absolute path, it is relative to project_root/tool/tool_definition/
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A Tool object parsed from the YAML file.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
if not yaml_path.is_absolute():
|
|
70
|
+
yaml_path = Path(__file__).parent / "tool_definition" / yaml_path
|
|
71
|
+
|
|
72
|
+
with open(yaml_path, "r") as f:
|
|
73
|
+
tool_dict = yaml.safe_load(f)
|
|
74
|
+
return Tool(**tool_dict)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
from pprint import pprint
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
from browser_use import Agent, AgentHistoryList, Browser, ChatAzureOpenAI
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def main(args: list[str]) -> int:
|
|
14
|
+
if len(args) > 1:
|
|
15
|
+
print("browse allows only one arg at a time.")
|
|
16
|
+
return 1
|
|
17
|
+
|
|
18
|
+
if not args:
|
|
19
|
+
print("Usage: browse <arg>")
|
|
20
|
+
return 1
|
|
21
|
+
|
|
22
|
+
what_to_browse = args[0]
|
|
23
|
+
|
|
24
|
+
browser = Browser(
|
|
25
|
+
headless=True
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
agent = Agent(
|
|
29
|
+
task=what_to_browse,
|
|
30
|
+
browser=browser,
|
|
31
|
+
llm=ChatAzureOpenAI(model="gpt-4.1"),
|
|
32
|
+
use_vision=False,
|
|
33
|
+
)
|
|
34
|
+
history: AgentHistoryList = await agent.run()
|
|
35
|
+
print("Final Result:")
|
|
36
|
+
pprint(history.final_result(), indent=4)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sys.exit(asyncio.run(main(sys.argv[1:])))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseTool(ABC):
|
|
5
|
+
@property
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def name(self) -> str:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def installation_command(self) -> str:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def verification_command(self) -> str:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def usage_instructions_to_llm(self) -> str:
|
|
23
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from microbot.tool_definitions.base_tool import BaseTool
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Ctags(BaseTool):
|
|
5
|
+
|
|
6
|
+
_instance = None # Class-level attribute to store the single instance
|
|
7
|
+
name = "ctags"
|
|
8
|
+
installation_command = "sudo apt install universal-ctags"
|
|
9
|
+
verification_command = "ctags --version"
|
|
10
|
+
usage_instructions_to_llm = (
|
|
11
|
+
"To use ctags, you can run 'ctags -R .' in your project directory"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def __new__(cls, *args, **kwargs):
|
|
15
|
+
if cls._instance is None:
|
|
16
|
+
cls._instance = super(Ctags, cls).__new__(cls)
|
|
17
|
+
return cls._instance
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
# We can still use __init__ for any additional setup if needed
|
|
21
|
+
if not hasattr(self, "initialized"):
|
|
22
|
+
self.initialized = True
|
|
23
|
+
print(f"Singleton instance initialized with name: {self.name}")
|
|
24
|
+
else:
|
|
25
|
+
print(f"Attempted to re-initialize an existing instance.")
|